Java集合中的链表
前情提要:在学习 ArrayList 时,认识到由于其底层是一段连续空间,当在 ArrayList 任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此 ArrayList 不适合做任意位置插入和删除比较多的场景。因此:java 集合中又引入了LinkedList,即链表结构。
目录
一、链表概念与结构
二、当向单链表的实现
2.1 准备工作
2.2 初始化链表
2.3 打印数据、链表长度、查找数据
2.4 头插法
2.5 尾插法
2.6 任意位置插入节点
2.7 删除第一次出现 key 的节点
2.8 删除链表中所有数据为 key 的节点
2.9 清空
三、面试题
一、链表概念与结构
链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序的通过链表中的引用链接次序实现的。
其结构总共有8种:
带头链表中的第一个节点相当于一个标记,如果采用头插法新增节点,只能在当前头的下一个节点插入;而如果是不带头的链表,头插法是在第一个节点前面新增节点。
二、当向单链表的实现
整个实现将围绕下图展开:
定义一个 cur 对象,表示当前节点。用法:遍历链表。
注意
1、cur 为空和 cur.next 为空的区别。 2、单向链表不能回退。
2.1 准备工作
1、先定义一个类用来表示整个链表,名为 SingleLinkedList。
2、而链表中包含节点,每个节点带有两个域,一个是数据域存储整型数据;另一个是指针域,指针域中存放的是下一个 节点 的引用,因此其类型也就是节点;且注意两个属性都用 public 修饰,在测试时大有用途。将每个节点封装成链表的内部类。
3、定义一个头部节点,用于查找或者输出链表数据,但这里并不代表链表带头。
4、将对链表的操作封装成一个接口,其功能包括打印链表数据 display、计算链表长度 size、查找数据 contains、头插法插入数据 addFirst、尾插法插入数据 addLast、任意位置插入数据 addIndex、删除第一次出现 key 的节点 remove、删除所有值为 key 的节点 removeAllKey、清空。
SingleLinkedList 链表:
package test;// 不带头 单向 链表
public class SingleLinkedList implements ILink{static class ListNode{ // 每一个 节点 都是对象,具有共同属性,因此可以抽象成内部类public int val;public ListNode next; // 节点的地址,类型也应该是节点的类型public ListNode(int val){this.val = val;}}public ListNode head; // 头节点@Override// ......
}
ILink 接口:
package test;/*** 链表的操作*/
public interface ILink {//头插法void addFirst(int data);//尾插法void addLast(int data);//任意位置插入,第一个数据节点为0号下标void addIndex(int index,int data);//查找是否包含关键字key是否在单链表当中boolean contains(int key);//删除第一次出现关键字为key的节点void remove(int key);//删除所有值为key的节点void removeAllKey(int key);//得到单链表的长度int size();// 清空void clear();// 打印链表数据void display();
}
在实现具体的操作之前,还需要考虑链表是否为空,即 head == null ?
2.2 初始化链表
在具体实现各项功能之前,需要有一个原始的链表,利于展示操作效果。其定义方法如下
package test;public class SingleLinkedList implements ILink{// ......public void creatList(){ // 创建一个原始链表ListNode node1 = new ListNode(1);ListNode node2 = new ListNode(2);ListNode node3 = new ListNode(3);ListNode node4 = new ListNode(4);this.head = node1;node1.next = node2;node2.next = node3;node3.next = node4;}// ......
}
2.3 打印数据、链表长度、查找数据
这三个功能都需要将链表从头到尾遍历一遍。而遍历的条件只是当前节点不为空。
package test;public class SingleLinkedList implements ILink{// ......@Overridepublic void display() {ListNode cur = head;while(cur != null){System.out.print(cur.val + " ");cur = cur.next;}System.out.println(); // 换行}@Overridepublic int size() {ListNode cur = head;int count = 0;while (cur != null){count++;cur = cur.next;}return count;}@Overridepublic boolean contains(int key) {ListNode cur = head;while (cur != null){if(cur.val == key){return true;}cur = cur.next;}return false;}
}
package test;public class TestSingle {public static void main(String[] args) {// ILink iLink = new SingleLinkedList(); // 通过接口无法调用 creatList()SingleLinkedList linkedList = new SingleLinkedList();linkedList.creatList(); // 还没实现新增节点功能前调用的原始链表linkedList.display();System.out.println(linkedList.size());}
}
2.4 头插法
@Override
public void addFirst(int data) {ListNode newNode = new ListNode(data);newNode.next = head;this.head = newNode;
}
即使原先的链表是空的,代码一样能实现头插法插入节点。
2.5 尾插法
@Override
public void addLast(int data) {ListNode newNode = new ListNode(data);if (head == null){ // 原来链表中一个节点都没有的情况下head = newNode;return;}ListNode cur = head;while (cur.next != null){cur = cur.next;} // 循环走完说明 cur.next == null 了cur.next = newNode;
}
package test_8_7;public class TestSingle {public static void main(String[] args) {SingleLinkedList linkedList = new SingleLinkedList();// 实现新增节点功能之后,不需要调用 creatList(),也可以创建链表// 头插法linkedList.addFirst(11);linkedList.addFirst(22);linkedList.addFirst(33);linkedList.addFirst(44);// 尾插法linkedList.addLast(55);linkedList.display();System.out.println(linkedList.size());}
}
2.6 任意位置插入节点
注意
1、指定位置的合法性;
2、指定下标为 0 和 指定位置等于链表长度 的情况分开处理;
3、单向链表不能回退,因此注意 cur 的位置;
4、所有的插入,优先绑定后面的节点。
@Overridepublic void addIndex(int index, int data) throws IllegalIndexException{int len = this.size(); // 存储当前长度,好处:提高效率if (index < 0 || index > len){throw new IllegalIndexException("指定位置不合法!!!");}else {if (index == 0){this.addFirst(data);return;}if (index == len){this.addLast(data);return;}ListNode newNode = new ListNode(data);ListNode cur = head;while (index - 1 != 0){cur = cur.next;index--;}newNode.next = cur.next;cur.next = newNode;}}
package test;// 处理指定位置的不合法异常
public class IllegalIndexException extends RuntimeException{public IllegalIndexException(){}public IllegalIndexException(String msg){super(msg);}
}
2.7 删除第一次出现 key 的节点
@Overridepublic void remove(int key) {if (head == null){return;}if(head.val == key){head = null;}ListNode cur = findNodeOfKey(key);if (cur == null){return;}ListNode del = cur.next;cur.next = del.next;}public ListNode findNodeOfKey(int key){ // 找到存储的数据是key的对象,并返回ListNode cur = head;while (cur.next != null){if (cur.next.val == key){return cur;}cur = cur.next;}return null;}
提示
1、在链表中查找 key 可封装成一个方法
2、查找 key 的条件 cur.next 不为空的前提下,已经判断了 cur.next 存储的数据是否等于 key 了
2.8 删除链表中所有数据为 key 的节点
@Overridepublic 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;}}
2.9 清空
像之前“清空” ArrayList 的方法在链表中是不可行的,因为如果引用一直存在,那么引用所指的对象无法被内存回收,容易造成内存泄漏,因此只有将 next 都置空了才算清空了链表数据。
@Overridepublic void clear() {ListNode cur = this.head;while (cur != null){ListNode prev = cur.next;cur.next = null;cur = prev;}head = null;}
三、面试题
3.1 删除链表中等于给定值 key 的所有节点。(与上面的 2.8 一个意思)
3.2 反转一个单链表。
public ListNode reverseList() { // 反转if(head == null){return null;}ListNode cur = head.next;head.next = null;while(cur != null){ListNode curNode = cur.next;cur.next = head;head = cur;cur = curNode;}return head;}
3.3 返回链表中的中间节点,如果有两个中间节点,则返回第2个节点。
提示,不能通过计算链表长度的方法进行编程。
那么,采用双指针的方法,快指针走两步,慢指针走1步,最后慢指针指向的就是中间。
public ListNode middleNode() { // 找中间节点ListNode fast = head; // 快指针,走两步ListNode slow = head; // 慢指针,走一步while(fast != null && fast.next != null){ // &&前面必须是 fast != null 的条件fast = fast.next.next;slow = slow.next;}/*while(fast.next != null && fast != null){ // 输出 NullPointerExceptionfast = fast.next.next;slow = slow.next;}*/return slow;}
3.4 输出该链表中倒数第 k 个节点。
虽然 leetcode 上已经保证了 k 的合法性,但自己实现的时候还是需要注意 k 的有效范围。
public int kthToLast(int k) { // 返回倒数第k个字节的数据if(head == null){return -1;}// 注意k的范围if (k <= 0){return -1;}/*if (k > 5){ // 这样写还得求链表的长度,不好return -1;}*/ListNode fast = head;ListNode slow = head;int count = 0;while(count != k-1){ // 为了与慢指针拉开一定的距离fast = fast.next;if(fast == null) // 这里就是判断k是否超出链表长度的条件return -1;count++;}while(fast.next != null){fast = fast.next;slow = slow.next;}return slow.val;}
3.5 合并两个有序链表并按升序排序成有序链表返回。
提示
1、创建一个头节点,作为标志,可存储数据,但该数据无实际意义;
2、返回的节点不是创建的头节点。
3、在自己实现的链表中实现时,需要注意,因为该方法操作的是两个链表,因此不好将它定义到 SingleLinkedList 这个类中,因此直接定义到测试类。但直接定义到测试类时因为 ListNode 是 SingleLinkedList 的内部类,需要用 SingleLinkedList. 来调用 ListNode
package test;public class TestSingle {public static SingleLinkedList.ListNode mergeTwoLists(SingleLinkedList.ListNode headA, SingleLinkedList.ListNode headB) { // 合并两个有序链表并排序SingleLinkedList.ListNode newHead = new SingleLinkedList.ListNode(-1); // 随便传一个数据,因为这个数据无效SingleLinkedList.ListNode tmp = newHead;while(headA != null && headB != null){if (headA.val < headB.val){tmp.next = headA;headA = headA.next;tmp = tmp.next;}else{tmp.next = headB;headB = headB.next;tmp = tmp.next;}}if (headA != null){tmp.next = headA;}if (headB != null){tmp.next = headB;}return newHead.next;}public static void main(String[] args) {// 测试合并两个链表SingleLinkedList linkedList = new SingleLinkedList();linkedList.addFirst(4);linkedList.addLast(11);linkedList.addLast(19);linkedList.addLast(44);SingleLinkedList linkedList2 = new SingleLinkedList();linkedList2.addFirst(3);linkedList2.addLast(6);linkedList2.addLast(32);linkedList2.addLast(57);SingleLinkedList.ListNode newNode = mergeTwoLists(linkedList.head, linkedList2.head);linkedList.display(newNode);}
}
3.6 以 x 值为基准将链表分割成两部分,所有小于 x 的节点排在大于或等于 x 的节点之后。
public ListNode partition(int x) { // 不改变原来数据的顺序下,将所有小于x的结点排在其余结点之前ListNode beforeStart = null; // 小于x的头节点ListNode beforeEnd = null; // 小于x的尾节点ListNode afterStart = null; // 大于x的头节点ListNode afterEnd = null; // 大于x的尾节点ListNode cur = head;while(cur != null){if(cur.val < x){if(beforeStart == null){ // 第一次插入beforeStart = beforeEnd = cur;}else{beforeEnd.next = cur;beforeEnd = beforeEnd.next;}}else{if(afterStart == null){afterStart = afterEnd = cur;}else{afterEnd.next = cur;afterEnd = afterEnd.next;}}cur = cur.next;}// 如果没有小于x的节点if (beforeStart == null){return afterStart;}// 前后两部分衔接起来beforeEnd.next = afterStart;// 第2部分的最后一个节点的引用要置空,否则编译超出范围if(afterStart != null){afterEnd.next = null;}return beforeStart;}
3.7 判断是否为回文链表。
提示
1、找到链表的中间节点;
2、将中间节点后面的节点反转;
3、头节点的值与最后一个节点的值进行比较,然后头节点往后,最后一个节点往前。
注意此题需要区分链表的节点数,如果是奇数比较好判断,但如果是偶数,需要考虑另一个判断条件。
奇数情况下:
偶数情况下:
判断条件是 fast.next = slow; 不能是 slow.next = fast;
且该判断条件前提是两者的数据是相等的,因此判断顺序很重要。
public boolean chkPalindrome(){ // 判断是否是回文链表if(head == null){return true;}// 1、找到中间节点ListNode fast = head;ListNode slow = head;while(fast != null && fast.next != null){fast = fast.next.next;slow = slow.next;}// 2、反转ListNode cur = slow.next;while(cur != null){ListNode curN = cur.next;cur.next = slow;slow = cur;cur = curN;}// 3、判断是否是回文while(head != slow){if (head.val != slow.val){return false;}// 如果链表是偶数的话,应该是下面的判断条件// 前提是两个val值是相等的if (head.next == slow){ // 注意是head的下一个节点是slow才对,因为slow的下一个节点回到了slow后面了。return true;}head = head.next;slow = slow.next;}return true;}
3.8 输入两个链表,找出它们的公共节点。
public SingleLinkedList.ListNode getIntersectionNode(SingleLinkedList.ListNode headA, SingleLinkedList.ListNode headB) { // 两链表是否相交SingleLinkedList.ListNode pLong = headA; // 假设链表A的长度比链表B的长SingleLinkedList.ListNode pShort = headB;int lenA = 0; // 链表A的长度int lenB = 0; // 链表B的长度while (pLong != null){lenA++;pLong = pLong.next;}while (pShort != null){lenB++;pShort = pShort.next;}// 因为在计算长度时改变了两个值,因此要从新定义pLong = headA;pShort = headB;// 计算两个链表长度的差值int len = lenA - lenB;if (len < 0){ // 如果len为负数,说明lenB大于lenA,规定pLong一定指向长的链表pLong = headB;pShort = headA;len = lenB - lenA; // 记住要把差值换成正数}// 长的链表先走len步while (len != 0){pLong = pLong.next;len--;}// 两个引用同时走,知道它们相遇while (pLong != pShort){pLong = pLong.next;pShort = pShort.next;}if (pLong == null){return null; // 说明没有相交}return pLong;}
测试:
public static void creatIntersect(SingleLinkedList.ListNode headA, SingleLinkedList.ListNode headB){ // 自己实现相交headA.next.next = headB.next.next.next;}public static void main(String[] args) {SingleLinkedList linkedList = new SingleLinkedList();linkedList.addFirst(5);linkedList.addLast(1);linkedList.addLast(8);linkedList.addLast(4);linkedList.addLast(5);SingleLinkedList linkedList2 = new SingleLinkedList();linkedList2.addFirst(4);linkedList2.addLast(1);linkedList2.addLast(6);linkedList2.addLast(8);linkedList2.addLast(4);linkedList2.addLast(5);creatIntersect(linkedList.head, linkedList2.head);SingleLinkedList.ListNode ret = getIntersectionNode(linkedList.head, linkedList2.head);System.out.println(ret.val);}
3.9 判断链表中是否有环。
* 进阶:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
public boolean hasCycle(){ // 判断当前链表是否有环if (head == null){return false;}ListNode fast = head;ListNode slow = head;while (fast != null && fast.next != null){fast = fast.next.next;slow = slow.next;if (fast == slow){return true;}}return false;}public void creatLoop(){ // 创建环ListNode cur = head;while (cur.next != null){cur = cur.next;}cur.next = head.next.next.next;}public ListNode detectCycle() { // 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULLListNode fast = head;ListNode slow = head;while( fast != null && fast.next != null){fast = fast.next.next;slow = slow.next;if(fast == slow){break;}}if(fast == null || fast.next == null){return null; // 说明没有环}slow = head;while(fast != slow){fast = fast.next;slow = slow.next;}return slow;}
测试类:
public class TestSingle {public static void main(String[] args) {// 测试返回链表开始入环的第一个节点。SingleLinkedList linkedList = new SingleLinkedList();linkedList.addFirst(5);linkedList.addLast(1);linkedList.addLast(8);linkedList.addLast(4);linkedList.addLast(5);linkedList.creatLoop();System.out.println(linkedList.hasCycle());SingleLinkedList.ListNode ret = linkedList.detectCycle();System.out.println(ret.val);}
}