Java数据结构:单链表
一、ArrayList的缺陷
- 1. ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
那么如何解决以上的问题呢?
—— java 集合中又引入了LinkedList,即链表结构。而本文章先介绍单链表及它的模拟实现。
二、链表
链表的结构和概念
概念
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
链表是由一个个节点组成的,比如单链表,每个节点都由两部分组成 该节点要存储的数据 和 存储的下一个节点的地址 。
单链表结构如上图,在C语言阶段,学习的链表结构是通过结构体来存储每个节点的,所以一个节点是一个结构体类型,其中next是一个指向下一个节点的指针,这个指针是这个结构体类型的指针;
在Java中,没有指针,也没有结构体,Java中使用内部类来代替(通常是静态内部类),即每一个节点是一个内部类类型,其中next是一个该内部类类型,即为引用类型。
(next引用是指向下一个节点的地址,而一个节点是一个内部类类型,所以next是一个内部类类型,C语言中的next指针相同道理)
除此之外,这个内部类还包含一个构造方法,用来初始化 val 的值,对于 next 引用,不需要使用构造方法对其进行初始化,因为引用类型默认的初始值是null,也就是说,如果单链表中所有节点定义完成后,不需要像C语言的链表一样额外再将最后一个节点的next引用置为null。
//静态内部类
static class ListNode {public int val;public ListNode next;//默认初始值nullpublic ListNode(int val) { //初始化valthis.val = val;}
}
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
结构
1.单向或者双向
单向:只能从一个方向遍历,即→指向的方向,从前往后。
双向:可以从两个方向遍历,即从前往后或者从后往前。
2.带头或者不带头
带头:指的就是链表中有哨兵位节点,该哨兵位节点即头节点
在实现单链表中,我们提到的头节点实际上指的是第一个有效节点(我们口头上提到的头节点也是),这不是正确的称呼,但是为了好理解才这样错误的称呼为头节点,实际在链表中,头节点指的是哨兵位,哨兵位节点不存储任何有效元素,但是它指向的是一个有效的地址,只是站在这里“放哨 的”,因此,无论哨兵节点中存放什么值都无所谓。
3.循环或者不循环
不循环:尾节点的next是null。
循环:一直循环,尾节点的next是头节点。
虽然有这么多的链表的结构,但是我们重点掌握两种:
- 不带头单向不循环链表 —— 单链表 :一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如 哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 不带头双向链表 —— 双链表 :在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。
所以,定义完单链表后,还需要知道单链表的头head的位置,才能完成增删查改的操作,才能知道从哪个位置开始遍历等。
一个节点是一个内部类类型,那么head头节点是一个内部类类型:
单链表定义:
static class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val = val;}
}
//头节点
public ListNode head;
三、单链表实现(实现单链表增删查改方法)
模拟实现单链表中的 增删查改方法 思路:
首先创建一个ISingleList接口,在这个接口中有增删查改等抽象方法,再创建一个MySingleList类,表示单链表,最后让MySingleList类实现接口中的抽象方法。
1.ISingleList接口完整框架
public interface ISingleList {//打印单链表void display();//获取单链表长度int size();//查找单链表中是否包含关键字key/key是否在单链表中void contains(int key);//头插void addFitst(int date);//尾插void addLast(int date);//任意位置插入/指定位置插入void addIndex(int index,int date);//删除值为key的节点void remove(int key);//删除所有值为key的节点void removeAllKey(int key);//清理单链表 void clear();
}
2.MySingleList类完整框架
在该类中,我们需要写一个静态内部类,表示一个节点;且这个类实现了ISingleList接口,因此,要重写抽象方法。
public class MySingleList implements ISingleList {static class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val = val;}}//头节点public ListNode head;//重写接口中的抽象方法public void addFirst(int date) {}…………………………}
现在,类和接口的整体框架已经说明完毕,现在,详细说明如何重写抽象方法
[1] display() 打印单链表
将链表中所有节点的值打印出来
public void display() {ListNode cur = head;while(cur != null) {System.out..print(cur.val + " ");cur = cur.next;}System.out.println();
}
注意:display方法并不属于单链表中的方法,这个方法是为了方便测试而写的。
cur != null 和 cur.next != null 的区别
- cur != null 用于检查当前节点是否存在。通常使用在遍历链表的场景。
- cur.next != null 用于检查当前节点的下一个节点是否存在。在使用cur.next之前,必须确保cur本身不为null,否则会抛出空指针异常。通常使用在删除操作、或者需要找链表尾节点的操作等。
[2] size() 获取单链表长度
定义一个len变量,表示在遍历链表时,记录节点的个数,每遍历一个,len++,这样就能知道长度。
public int size() {int len = 0;ListNode cur = head;while(cur != null) {len++;cur = cur.next;}return len;
}
[3] contains() 关键字key是否在单链表中/是否包含key
依然需要遍历链表,如果其中一个节点的val值刚好等于key,则返回true,如果这个节点不等于key,则继续往后走,直到最后都没有找到,则返回false。
public boolean contains(int key) {ListNode cur = head;while(cur != null) {if(cur.val == key) {return true;}cur = cur.next;} return false;
}
[4] addFirst() 头插
该方法表示在原链表的头节点前插入一个新节点,首先需要创建一个新节点,然后在进行插入操作
如上图,列出了头插中的三种情况,也可以说是两种情况:为空和不为空。
发现,其实它们的做法是完全一样的 ——
node.next=head; head=node
public void addFirst(int date) {ListNode node = new ListNode(date);//创建新节点node.next = head;//将node变成新的头节点head = node;//更新头节点
}
[5] addLast() 尾插
该方法表示在原链表的尾节点后插入一个新节点
public void addLast(int date) {ListNode node = new ListNode(date);//链表为空if(head == null) {head = node;return;}//不为空//找尾ListNode cur = head;while(cur.next != null) {cur = cur.next;}//此时的cur就是尾节点cur.next = node;//更新node为新的尾节点
}
[6] addIndex() 任意位置插入/指定位置插入
该方法表示在原链表的任意一个位置插入一个新节点
除了上图所示的所有做法之外,还需要对 index 位置的合法性进行判断,即index不能小于0,或者不能大于链表的长度size(),写一个方法checkIndex来判断合法性;这里仿照顺序表模拟的时候,也用异常类的知识,如果index不合法时,就会对这个异常进行捕获并处理。
(6.1) IndexIllegalException 异常类
public class IndexIllegalException extends RunTimeException {public IndexIllegalException() {super();}public IndexIllegalException(String memage) {super(memage);}
}
(6.2) checkIndex() 判断index位置合法性
private void checkIndex(int index) throws IndexIllegalException {if(index < 0 || index > size()) {throw new IndexIllegalException("index位置不合法!");}
}
(6.3) addIndex()
public void addIndex(int index,int date) {try {checkIndex(index);//头插情况if(index == 0) {addFirst(date);return;}//尾插情况if(index == size()) {addLast(date);return;}//中间ListNode node = new ListNode(date);//创建新节点//找到index位置节点的前一个节点ListNode cur = head;while(index-1 != 0) {cur = cur.next;index--;}//此时的cur就是index的前一个节点//插入node.next = cur.next;cur.next = node;}catch(IndexIllegalException e) {e.printStackTrace();}
}
[7] remove() 删除值为key的节点
该方法删除的是第一个值为key的节点,即如果出现多个值为key的节点,只会删除第一个节点;如果没有出现,则正常使用。
如果是空链表,则不需要删除,直接返回;
如果是非空链表:那么就要进行删除。
大体思路:假设要删除的节点为del,需要先找到del的前一个节点(遍历链表,如果发现cur的下一个节点的值是key,那么cur就是要删除的节点的前一个节点),我们可以写一个方法findNodeOfKey() 来表示这个功能,返回值是ListNode,如果能够找到前一个节点,就返回这个节点,如果找不到,也就是链表中根本就没有要删除的key值以及它所在的节点,那么就返回null。
如果找到了cur节点,那么执行删除操作,如下图:
(7.1) findNodeOfKey() 找到值为key节点的前一个节点
private ListNode findNodeOfKey(int key) {ListNode cur = head;while(cur.next != null) { //注意这里是cur.next != null,确保next引用不为空if(cur.next.val == key) { //如果cur节点的下一个节点的val值等于keyreturn cur;//则说明cur是要删除的节点del的前一个节点,返回cur}cur = cur.next}return null;//没有找到返回null
}
(7.2) remove()
public void remove(int key) {//链表为空if(head == null) {return;}//非空ListNode cur = findNodeOfKey(key);if(cur == null) { //在findNodeOfKey方法中cur走到null,也就是说链表中没有你要删除的key值return;}//链表中有要删除的key值,删除该值所在的节点ListNode del = cur.next;//del节点是cur的下一个节点cur.next = del.next;//删除
}
[8] removeAllKey() 删除所有值为key的节点
该方法和remove方法做法相似,但是,它是删除所有key值的节点。
如果是空链表,就不需要删除,直接返回。
如果是非空情况:如下图,定义一个prev和cur引用,prev初始表示head的位置,cur初始表示head.next位置,遍历链表,如果cur节点的val值等于key,则删除节点——prev.next=cur.next;
如果cur节点的val值不等于key,则cur继续往后走,而cur在往后走之前,prev先保存cur节点位置,即prev=cur; cur=cur.next;
还有另一种情况,如果链表的第一个节点的val值也是key的情况:
那么按上述的做法,第一个节点的位置是永远删不了的,解决方法——就是第一个节点如果是val==key的话,单独进行删除;即head.val==key,进行删除,头节点被删除掉,那么第二个节点就变成新的头节点,即head=head.next;
注意:处理头节点是key的情况这段代码必须放在最后进行,原因——确保如果删除完后,新的头节点如果还是key,那么由于这段代码是在最后进行的,那么就可以每次都检查一遍头节点是否是key。
public void removeAllKey(int key) {//链表为空if(head == null) {return;}//非空ListNode prev = head;ListNode cur = head.next;while(cur != null) {if(cur.val == key) {prev.next = cur.next;cur = cur.next;}else {prev = cur;cur = cur.next;}}//处理头节点是key的情况if(head.val == key) {head = head.next;}
}
[9] clear() 清理单链表
链表之所以能够只要知道head头节点,就能知道其他节点中的数据,是因为每个节点中有next引用,通过next,节点之间建立了联系。
而我们可以利用这一点,进行单链表的清理操作:
遍历链表,定义一个cur引用,它初始表示head,定义curNext,它初始表示head.next,即cur.next;
通过遍历链表的方式,将每个节点的next引用变为null,切断节点之间的联系,不过在切断之前,先保存一下cur的下一个节点,即 curNext=cur; cur.next=null
通过上述的操作,会得到如下图的结果,这样每个节点之间是独立的,至于节点中的val数据,因为没有被使用,会被自动回收的,因此不需要理会;这样就达到了清理单链表的结果。
但是,还有一个问题,就是我们需要最后进行head头节点清空操作,即head=null,原因——
虽然已经切断了节点间的联系,但是通过head不能再知道其他节点的数据,但是head自身的数据还是知道的,这样并没有彻底清理干净,例如,如果打印这段链表的话,还能打印出head的数据12,因此,需要手动清空head。
public void clear() {ListNode cur = head;while(cur != null) {ListNode curNext = cur.next;//保存cur的下一个节点cur.next = null;//断开节点间的引用cur = curNext;//移动指针到下一个节点}head = null;//清空头节点
}
四、单链表完整代码
//ISingleLIst接口public interface ISingleList {//打印单链表void display();//获取单链表长度int size();//链表中是否包含值为key的节点boolean contains(int key);//头插void addFirst(int date);//尾插void addLast(int date);//任意位置插入void addIndex(int index,int date);//删除值为key的节点void remove(int key);//删除所有值为key的节点void removeAllKey(int key);//清理单链表void clear();
}
//MySingleList类public class MySingleList implements ISingleList {static class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val = val;}}public ListNode head;public void display() {ListNode cur = head;while(cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println();}public int size() {int len = 0;ListNode cur = head;while(cur != null) {len++;cur = cur.next;}return len;}public boolean contains(int key) {ListNode cur = head;while(cur != null) {if(cur.val == key) {return true;}cur = cur.next;}return false;}public void addFirst(int date) {ListNode node = new ListNode(date);node.next = head;head = node;}public void addLast(int date) {ListNode node = new ListNode(date);if(head == null) {head = node;return;}//找尾ListNode cur = head;while(cur.next != null) {cur = cur.next;}cur.next = node;}private void checkIndex(int index) throws IndexIllegalException {if(index < 0 || index > size()) {thow new IndexIllegalException("index不合法!");}}public void addIndex(int index,int date) {try {checkIndex(index);if(index == 0) {addFirst(date);return;}if(index == size()) {addLast(date);return;}ListNode cur = head;while(index-1 != 0) {cur = cur.next; index--;}ListNode node = new ListNode(date);node.next = cur.next;cur.next = node;}catch(IndexIllegalException e) {e.printStackTrace();}} private ListNode findNodeOfKey(int key) {ListNode cur = head;while(cur.next != null) {if(cur.next.val == key) {return cur;}cur = cur.next;}return null;}public void remove(int key) {if(head == null) {return;}//非空ListNode cur = findNodeOfKey(key);if(cur == null) {return;}ListNode del = cur.next;cur.next = del.next;} public void removeAllKey(int key) {if(head == null) {return;}ListNode prev = head;ListNode cur = head.next;while(cur != null) {if(cur.val == key) {prev.next = cur.next;cur = cur.next;}else {prev = cur;cur = cur.next;}}if(head.val == key) {head = head.next;}}public void clear() {ListNode cur = head;while(cur != null) {curNext = cur.next;cur.next = null;cur = curNext;}head = null;}
}
//IndexIllegalException异常类public class IndexIllegalException {public IndexIllegalException() {super();}public IndexIllegalException(String memage) {super(memage);}
}