🎓??引言
??顺序表的问题及思考,之前我写了一篇顺序表的博客,发现数组和顺序表基本一样,很简单地就实现了,但存在一些问题比如:
- 顺序表中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长也可以是其他,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
那么 如何解决以上问题呢?答案是我们可以通过链表来解决上面存在的问题,链表适合插入和删除元素,可以做到随用随取不浪费空间,顺序表则适合给定要查找元素的下标进行查找,
各有各的有点,下面给出了链表的结构来简单地看一下。
🎓??链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用变量来实现的 ,如下图的链表: 下面是链表节点的基本结构: 实际中链表的结构非常多样,以下情况组合起来就有8种链表结构: 单/双向—>带/不带头——>循环/非循环(2x2x2)
- 单向带头循环
- 双向带头循环
- 单向带头非循环
- 双向带头非循环
- 单向不带头循环
- 双向不带头循环
- 单向不带头非循环
- 双向不带头非循环
📝📐本篇博客重点实现单链表的基本功能,双链表后期博文讲解,而单向链表中,比较难,重要,面试经常遇到的就是单向不带头非循环的链表了,因此我们重点讲解它。单向的带头和循环的很简单,我们把不带头的非循环的明白了,其他的学习起来也很简单了。 注意事项: 对于这里所说的链表带不带头,我们需要好好理解下,
- 不带头的单向非循环链表,这个链表没有真正的头结点,但是我们把第一个节点叫做头结点,这个节点是个虚拟的,只起一个标识作用,标识这个链表的头结点
- 这个头结点的位置随时可能发生这变化,是不固定的,之后通过这个头结点我们要完成一些链表的增删查改。从头开始,因此头很重要
- 带头结点的链表,有正真的头结点,这个头结点也是用来标识头结点的位置的,这个节点的位置不会发生改变, 但这个头结点里面的值不被使用,带头的就很简单,比如每次头插,头都不变。
下面看代码的时候你就会真正地感受到了,下面是几种循环的结构:
单向不带头非循环链表 单向带头非循环链表 单向不带头循环链表
🎓??单链表的实现及图示分析
通过分析上面的链表,我们可以抽象出两个类,一个是链表本身是一个类(链表这种类型)定义在MyLinkedList.java这个java文件中,另外链表里面的节点也可以看做一个类,定义在Node.java这个文件中,这两个链表里面有不同的成员属性,利用面向对象的思想来实现我们所要的单链表。
📝🔷单链表之创建单链表
以穷举的方式创建单链表 分别实例化了四个对象,并且调用构造方法进行值的初始化,在通过节点里面引用变量的指向使所有节点连接起来。
public void createList() {
Node node1 = new Node(12);
Node node2 = new Node(3);
Node node3 = new Node(5);
Node node4 = new Node(2);
node1.next = node2;
node2.next = node3;
node3.next = node4;
this.head = node1;
}
📝🔷单链表之计算单链表的长度
这个很简单遍历链表元素就行, 创建一个临时节点,代替头结点遍历,头结点的位置很重要起到标识作用不能随便改变, 没有了头就不能找到这个链表了,还要注意遍历的结束条件。
public int size() {
Node cur = this.head;
int count = 0;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
📝🔷单链表之打印单链表
打印单链表也很简单,遍历打印就行,只是链表元素向后移动是通过引用变量指向对象的移动移动的。
public void show() {
Node cur = this.head;
while(cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
📝🔷单链表之单链表的增删查改
📝🔷头插法插入元素
头插法插入元素的时候需要注意:
- 在插入一个节点的时候,一定要先使插入节点的空指针域指向下一个节点的地址,再更换头结点
- 新插入的节点是头结点
- 如果插入时,head为空,则证明之前链表没有数据,直接this.head=node即可,之后又按第一个节点不是null的方法插入即可
public void addFirst(int data) {
Node node = new Node(data);
if(this.head == null) {
this.head = node;
}else {
node.next = this.head;
this.head = node;
}
}
🐮🐵尾插法插入元素
如果一个链表为空,也就是头结点head==null,直接插入一个元素既是头也是尾,不为空的时候通过while循环找到尾巴节点,**当 cur.next为null时候是尾巴节点,**再使这个尾巴节点里面存储node节点的地址,这样把所有的节点都连接起来了。
public void addLast(int data) {
Node node = new Node(data);
if(this.head == null) {
this.head = node;
}else {
Node cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
🐮🐵把一个节点插入到链表的任意位置
对于链表里面的元素,假设第一个元素的下标为0,第二个元素下标为2,以此类推,插入元素之前要判断元素位置的合法性,插入位置,0=<index<=size,当插入位置为0的时候,相当于头插法,当index=size的时候,相当于尾插法,插入元素在中间的时候,插入到插入位置的前面,之后连接前后两个元素即可。
寻找插入点的前一个节点
public Node searchPrev(int index) {
Node cur = this.head;
int count = 0;
while(count != index-1) {
cur = cur.next;
count++;
}
return cur;
}
中间插入元素
public void addIndex(int index,int data){
if(index < 0 || index > size()) {
System.out.println("下标不合法");
return;
}
if(index == 0) {
addFirst(data);
return;
}
if(index == size()) {
addLast(data);
return;
}
Node cur = searchPrev(index);
Node node = new Node(data);
node.next = cur.next;
cur.next = node;
}
🐮🐵查找链表中是否含有某个元素
从链表的头到尾遍历,看是否含有某个值, 注意遍历链表的条件是cur==null;
public boolean contains(int val) {
Node cur = this.head;
while (cur != null) {
if(cur.val == val) {
return true;
}
cur = cur.next;
}
return false;
}
🐮🐵删除链表中第一次出现value值的节点
删除链表中第一次出现value值的节点,首先这个链表要不为空,为null就直接返回什么都不用做了,不为空,找到要删除节点的前面一个节点,**使前驱节点连接要删除节点后面的一个节点就把那个值给删除了,**另外如果头结点为删除的节点要单独判断,更换头结点就删除了。详细解释看图
public Node searchPrevNode(int val) {
Node cur = this.head;
while (cur.next != null) {
if(cur.next.val == val) {
return cur;
}
cur = cur.next;
}
return null;
}
public void remove(int val) {
if(this.head == null) return;
if(this.head.val == val) {
this.head = this.head.next;
return;
}
Node cur = searchPrevNode(val);
if(cur == null) {
System.out.println("没有你要删除的节点!");
return;
}
Node del = cur.next;
cur.next = del.next;
}
如果是头结点是要删除的节点
🐮🐵删除链表中出现value值得所有节点
👀😄删除链表中出现的value值的所有节点,和之前删除出现value值的方法差不多,定义一个prev标记要删除节点的前驱,找到要删除的节点跳过它就行了, 但要注意出现连续要删除的value值和头节点是要删除的节点的情况。
public void removeAllKey(int val) {
if(this.head == null) {
return;
}
Node prev = this.head;
Node cur = this.head.next;
while (cur != null) {
if(cur.val == val) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
if(this.head.val == val) {
this.head = this.head.next;
}
}
因为在删除节点时,**前驱节点一定是在判断是否要删除的节点cur的前面,**也就是说通过上面的方法删除节点,头结点的值没判断,要删除后面出现的value值后,要单独判断头结点的情况。
🐮🐵清空单链表
👀😄jvm有自动回收机制,当一个对象没有被引用的时候就被回收了,当然我们也可以手动的将其设置了不被引用,而回收,最简单的清空链表的方法把头结点置为null,它不引用别的节点,别的节点也都不被引用,整个链表就被jvm回收了,当然最好还是一个一个节点的next域置为null,使他们都不被引用。
public void clear() {
while (this.head != null) {
Node curNext = this.head.next;
this.head.next = null;
this.head = curNext;
}
}
📝📐有三个类文件,最后把所有链表功能代码放在代码组合在一起放在MyLinkedList.java就是完整功能代码了,下面是测试类的代码,用于测试我们单链表的功能,还有一个链表节点的类文件Node.java。
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(8);
myLinkedList.addLast(9);
myLinkedList.addLast(1);
myLinkedList.addLast(1);
myLinkedList.addLast(16);
myLinkedList.addLast(7);
myLinkedList.show();
myLinkedList.removeAllKey(1);
myLinkedList.show();
System.out.println(myLinkedList.contains(21));
}
}
Node.java类文件
public class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
如🈶?题请🈯正,记🉐点👍🏻🍹持,🦀🦀,🦀🦀
|