目录
1.链表的结构与特点
1.1 链表的结构:
1.2 链表的特点:
2. 不带头单链表的模拟实现
3. 单链表OJ
3.1 题目1:移除链表元素:
3.2 题目2:反转一个单链表
3.3 题目3:返回链表的中间结点
3.4 题目4:链表的倒数第k个结点
3.5 题目5:合并两个有序链表
3.6 题目6:链表的回文结构
3.7 题目7:链表分割
3.8 题目8:相交链表
3.9 题目9:环形链表
3.10 题目10:环形链表Ⅱ
4. LinkedList的模拟实现
5. LinkedList的使用
6.LinkedList和ArrayList的区别
1.链表的结构与特点
1.1 链表的结构:
(1)链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的;
(2)现实中常见的链表有8种结构:
单向与双向,带头或不带头,循环或非循环,组合起来就有8种结构;
最常用的是:① 单向不带头非循环链表; ② 双向不带头非循环;
1.2 链表的特点:
(1)链表是由结点构成的,每个节点至少包括两个域:当前结点的数据与下一个结点的地址;
(2)链表的链式结构在逻辑上是连续的,但是物理上不一定连续;
(3)结点一般都是在堆上申请出来的;
2. 不带头单链表的模拟实现
(1)包类关系:
(2)MySingleList:
package TestMySingleList;
// 不带头的非循环单链表
public class MySingleList {static class Node{public int val; //存储数据public Node next; //存储下一个结点的地址public Node(int val){this.val = val;}}public Node head; // 代表当前链表的头结点的引用public void createLink(){Node node1 = new Node(12);Node node2 = new Node(45);Node node3 = new Node(23);Node node4 = new Node(90);node1.next = node2;node2.next = node3;node3.next = node4;head = node1;}//成员方法1:遍历并打印链表public void display(){Node cur = head;while(cur!=null){System.out.print(cur.val+" ");cur=cur.next;}}//重载:从指定位置开始遍历打印列表public void display(ListNode newHead){ListNode cur = newHead;while(cur!=null){System.out.print(cur.val+" ");cur = cur.next;}}//成员方法2:查找某个关键字key是否在单链表中public boolean contains(int key){Node cur = head;while(cur!=null){if(cur.val == key){// 若为引用类型需用equals方法return true;}cur=cur.next;}return false;}//成员方法3:得到链表长度(时间复杂度为O(N)public int size(){int count=0;Node cur = head;while(cur!=null){count++;cur=cur.next;}return count;}//成员方法4:头插法public void addFirst(int data){Node newNode = new Node(data);newNode.next = head;head = newNode;}//成员方法5:尾插法public void addLast(int data){Node newNode = new Node(data);newNode.next = null;// 判断链表是否为空(判断是否为首次插入)if(head==null){head = newNode;return;}Node cur = head;while(cur.next!=null){cur=cur.next;}cur.next = newNode;}//成员方法6:任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data) throws ListIndexOutOfException{if(index == 0){ // 头插addFirst(data);return;}if(index == size()) { // 尾插addLast(data);return;}Node cur = findIndexSubOne(index);Node newNode = new Node(data);newNode.next = cur.next;cur.next = newNode;}// 辅助方法:判断插入位置下标是否异常private void checkIndex(int index){if(index<0||index>size()){throw new ListIndexOutOfException("下表位置不合法!");}}// 辅助方法:查询目标下标结点的前结点private Node findIndexSubOne(int index){// 查找index-1位置的结点Node cur = head;int count=0;while(count!=index-1){cur=cur.next;count++;}return cur;}//成员方法7:删除第一次出现关键字为key的结点public void remove(int key){if(head==null){return; // 链表为空}if(head.val == key){head = head.next;return;}Node preDel = searchPrevNode(key);if(preDel == null){return;}Node del = preDel.next;preDel.next = del.next;}//辅助方法:查找目标key值结点的前结点private Node searchPrevNode(int key){Node cur = head;while(cur.next!=null){if(cur.next.val == key){return cur;}cur=cur.next;}return null; // 没有找到待删除的结点}//成员方法8:删除所有值为key的结点public void removeAllKey(int key){if(head==null){return;}Node cur = head.next;Node prev = head;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;}}//成员方法9:清空链表public void clear(){head =null;}
}
(3)ListIndexOutOfException:
package TestMySingleList;public class ListIndexOutOfException extends RuntimeException{public ListIndexOutOfException(){}public ListIndexOutOfException(String message){super(message);}
}
3. 单链表OJ
3.1 题目1:移除链表元素:
题目链接:203. 移除链表元素 - 力扣(LeetCode)
代码:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode removeElements(ListNode head, int val) {if(head==null){return null;}ListNode prev = head;ListNode cur = prev.next;while(cur!=null){if(cur.val==val){prev.next = cur.next;cur = cur.next;}else{prev = cur;cur = cur.next;}}if(head.val == val){head = head.next;}return head;}
}
3.2 题目2:反转一个单链表
题目链接:206. 反转链表 - 力扣(LeetCode)
解题思路:将原列表头结点head.next置为null,依次取头结点的下一个结点对头结点进行头插;
代码:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode reverseList(ListNode head) {if(head==null){return null;}if(head.next == null){return head;}ListNode cur = head.next;head.next = null;while(cur!=null){ListNode curNext = cur.next;// 头插法cur.next=head;head = cur; // 更新头结点cur = curNext; }return head;}
}
3.3 题目3:返回链表的中间结点
题目链接:876. 链表的中间结点 - 力扣(LeetCode)
解题思路:定义快慢指针,快指针每次走两步,慢指针每次走一步,当快指针到达链表末尾时,慢指针就指向链表中间结点位置;
代码:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode middleNode(ListNode head) {ListNode fast = head;ListNode slow = head;while(fast!=null && fast.next!=null){fast = fast.next.next;slow = slow.next;}return slow;}
}
注:(1)当链表结点数为奇数时,while循环截止条件是fast.next为空;当链表结点数为偶数时,while循环截止条件是fast为空;
(2)while循环的判断条件逻辑与前后条件不能顺序颠倒,因为当fast为空时,fast.next可能触发空指针异常;
3.4 题目4:链表的倒数第k个结点
题目链接:链表中倒数第k个结点_牛客题霸_牛客网
解题思路:定义快慢指针,令慢指针slow标记待找结点。
当fast指向尾结点时,倒数第k个结点即slow比fast慢k-1步;
故而,令fast走k-1步,slow开始走,两个指针开始同时前进,当fast.next为空(fast行至尾结点)时,slow所指节点就是待找结点;
代码:
import java.util.*;
/*
public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}*/
public class Solution {public ListNode FindKthToTail(ListNode head,int k) {// 判断 k 位置是否合法if(k<=0 || head==null){return null;}ListNode fast = head;ListNode slow = head;// fast 走 k-1 步while(k-1!=0){fast=fast.next;if(fast==null){return null;}k--;}// slow 开始和 fast 一起走while(fast.next!=null){fast = fast.next;slow = slow.next;}return slow;}
}
3.5 题目5:合并两个有序链表
题目链接:21. 合并两个有序链表 - 力扣(LeetCode)
代码:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode newHead = new ListNode(0);ListNode tmp = newHead;// 两链表均有数据while(list1!=null && list2!=null){if(list1.val < list2.val){tmp.next = list1;list1 = list1.next;tmp = tmp.next;}else{tmp.next = list2;list2 = list2.next;tmp = tmp.next;}}// 链表2已经走完,直接将链表1其余部分拼接到tmp结点后即可if(list1 != null){tmp.next = list1;}// 链表1已经走完,将链表2其余部分拼接到tmp结点后if(list2 != null){tmp.next = list2;}return newHead.next;}
}
3.6 题目6:链表的回文结构
题目链接:链表的回文结构_牛客题霸_牛客网
解题思路:寻找链表中间结点,将链表后半部分反转。从链表首位开始依次对应判断是否相等;
代码:
import java.util.*;/*
public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}*/
public class PalindromeList {public boolean chkPalindrome(ListNode head) {if(head == null){return false;}if(head.next == null){return true;}ListNode fast = head;ListNode slow = head;// 找中间结点while(fast != null && fast.next != null){fast = fast.next.next;slow = slow.next;}// 翻转ListNode cur = slow.next; // cur代表当前需要翻转的结点while(cur != null){ListNode curNext = cur.next;cur.next = slow;slow = cur;cur = curNext;}// slow从后往前, head从前往后 依次对应比较while(slow != head){if(head.val != slow.val){return false;}if(head.next == slow){return true;}slow = slow.next;head = head.next;}return true;}
}
注:在head与slow分别开始从两端对应比较时,对于结点个数为偶数个的链表,直接判断head与slow是否相遇会导致head与slow错过,需要增加一个判断:head.next是否为slow,如果是则说明是回文序列;
3.7 题目7:链表分割
题目链接:链表分割_牛客题霸_牛客网
代码:
import java.util.*;/*
public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;}
}*/
public class Partition {public ListNode partition(ListNode pHead, int x) {ListNode bs = null; // 前半段首结点ListNode be = null; // 前半段尾结点ListNode as = null; // 后半段首结点ListNode ae = null; // 后半段尾结点ListNode cur = pHead;while(cur != null){if(cur.val < x){if(bs == null){// 前半段首次插入bs = cur;be = cur;}else{be.next = cur;be = be.next;}}else{if(as == null){// 后半段首次插入as = cur;ae = cur;}else{ae.next = cur;ae = ae.next;}}cur = cur.next;}// 可能不是同时存在小于x和大于x数据的情况// 前半段为空直接返回后半段if(bs == null){ return as;}// 前半段不为空,则与后半段连接即可be.next = as;// 后半段不为空时,将后半段指针域置空if(as != null){ae.next = null; }return bs;}
}
3.8 题目8:相交链表
题目链接:160. 相交链表 - 力扣(LeetCode)
解题思路:定义两头结点指针,确定长短链表;
为了保证指针能同时到达两链表相交结点位置,令长链表头结点指针向前走两链表长度之差步;
令两链表头结点指针开始同步前进并对应判断是否引用相同并返回两引用任意其一即可;
代码:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) {* val = x;* next = null;* }* }*/
public class Solution {public int size(ListNode head){int len=0;ListNode cur =head;while(cur!=null){len++;cur = cur.next;}return len;}public ListNode getIntersectionNode(ListNode headA, ListNode headB) {// 求两个链表长度并确定长短链表int lenA = size(headA);int lenB = size(headB);int len = lenA - lenB;ListNode headLong = headA;ListNode headShort = headB;if(len < 0){ // 链表B比链表A长headLong = headB;headShort = headA;len = lenB - lenA;}// 令长链表头结点指针走两链表之差长度的距离while(len != 0){headLong = headLong.next;len--;}// 两链表头结点指针开始同步前进while(headLong != headShort){headLong = headLong.next;headShort = headShort.next;}// 判断是否两指针走完链表都未相遇,同时等于空值而相等if(headLong == headShort && headLong == null){return null;}return headLong;}
}
3.9 题目9:环形链表
题目链接:141. 环形链表 - 力扣(LeetCode)
代码:
/*** Definition for singly-linked list.* class ListNode {* int val;* ListNode next;* ListNode(int x) {* val = x;* next = null;* }* }*/
public class Solution {public boolean hasCycle(ListNode head) {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;}
}
注:判断是否带环的方法是快指针每次走两步慢指针走一步,两个指针相差的距离最大是一个环的长度,不会出现套圈的情况。而使用快指针每次三或四步等,慢指针走一步,可能会出现两指针永远无法相遇的情况;
3.10 题目10:环形链表Ⅱ
题目链接:142. 环形链表 II - 力扣(LeetCode)
(与上一题的区别在于,如果有环则返回链表开始入环的第一个结点)
代码:
/*** Definition for singly-linked list.* class ListNode {* int val;* ListNode next;* ListNode(int x) {* val = x;* next = null;* }* }*/
public class Solution {public ListNode detectCycle(ListNode head) {ListNode 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;}// 此时fast和slow在相遇点// 将slow置回头结点slow = head;// 令slow从头结点出发,fast从相遇点出发,二者再次相遇点即为环的入口点while(fast != slow){fast = fast.next;slow = slow.next;}return slow;}
}
4. LinkedList的模拟实现
LinkedList底层就是一个双向链表;
(1)包类关系:
(2)MyLinkedList:
package TestMyLinkedList;public class MyLinkedList {static class ListNode{public int val;public ListNode prev;public ListNode next;public ListNode(int val){this.val = val;}}public ListNode head;public ListNode last;// 遍历并打印链表public void display(){ListNode cur = head;while(cur != null){System.out.print(cur.val+" ");cur = cur.next;}System.out.println();}// 求链表长度public int size(){ListNode cur = head;int len=0;while(cur!=null){cur = cur.next;len++;}return len;}// 查找关键字key是否包含在链表中public boolean contains(int key){ListNode cur = head;while(cur != null){if(cur.val == key){return true;}}return false;}// 尾插 O(1)public void addLast(int data){ListNode newNode = new ListNode(data);if(head == null){head = newNode;last = newNode;}else{last.next = newNode;newNode.prev = last;last = newNode;}}// 头插public void addFirst(int data){ListNode newNode = new ListNode(data);if(head==null){head = newNode;last = newNode;}else{newNode.next = head;head.prev = newNode;head = newNode;}}// 任意位置插入,第一个数据节点为0号下标public void addIndex(int index, int data){if(index<0 || index>size()){throw new ListIndexOutOfException();}if(index == 0){addFirst(data);return;}if(index == size()){addLast(data);return;}ListNode newNode = new ListNode(data);ListNode cur = findIndex(index);ListNode preCur = cur.prev;preCur.next = newNode;newNode.prev = preCur;newNode.next = cur;cur.prev = newNode;}// 辅助方法:查询目标下标结点private ListNode findIndex(int index){ListNode cur = head;while(index!=0){cur = cur.next;index--;}return cur;}// 删除第一次出现关键词key的结点public void remove(int key){if(head == null){return;}if(head.val == key){head = head.next;return;}if(last.val == key){last = last.prev;last.next =null;return;}ListNode del = findKey(key);ListNode preDel = del.prev;ListNode delNext = del.next;preDel.next = delNext;delNext.prev = preDel;}// 辅助方法:查找关键词为key的结点public ListNode findKey(int key){ListNode cur = head;while(cur!=null){if(cur.val == key){return cur;}cur =cur.next;}return null;}// 删除链表中所有值为key的结点public void removeAllKey(int key){ListNode cur = head;while(cur!= null) {if (cur.val == key) {// 删除的是头结点if (cur == head) {head = head.next;// 只有一个结点if (head != null) {head.prev = null;}} else {// 删除的不是头结点cur.prev.next = cur.next;// 删除的不是尾结点if (cur.next != null) {cur.next.prev = cur.prev;} else {// 删除的是尾巴结点last = last.prev;}}}cur = cur.next;}}// 清空列表public void clear(){ListNode cur = head;ListNode curNext = cur.next;while(cur!=null){cur.val = 0;cur.next = null;cur.prev = null;cur = curNext;}head = null;last = null;}
}
(3)ListIndexOtOfException:
package TestMyLinkedList;public class ListIndexOutOfException extends RuntimeException{public ListIndexOutOfException(){}public ListIndexOutOfException(String message){super(message);}
}
注:其中删除所有值为key的结点方法removeAllKey也可以写为:
/ 删除链表中所有值为key的结点public void removeAllKey(int key){if(head == null){return;}while(head.val == key){head = head.next;if(head == null){return;}}if(last.val == key){last = last.prev;last.next = null;return;}ListNode cur = head;while(cur!=null){ListNode del = findKey(cur, key);if(del!=null) {ListNode preDel = del.prev;ListNode delNext = del.next;preDel.next = delNext;delNext.prev = preDel;}cur = cur.next;}}public ListNode findKey(ListNode node, int key){ListNode cur = node;while(cur != null){if(cur.val == key){return cur;}cur = cur.next;}return null;}
5. LinkedList的使用
Linkedlist的方法与ArrayList基本一致,此处不再详细举例,请参考ArrayList文章:
CSDN
注:(1)LinkedList使用其他集合容器构造方法含义:
(2)LinkedList的迭代器遍历法:
ListIterator<String> it =linkedList.listIterator();while(it.hasNext()){// 有下一个则打印下一个并向下走一步System.out.print(it.next());}ListIterator<String> it2 = linkedList.listIterator(linkedList.size());while(it2.hasPrevious()){// 有前一个则打印前一个并向前走一步System.out.print(it2.previous());}
6.LinkedList和ArrayList的区别
不同点 | ArrayList | LinkedList |
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定联系 |
随机访问 | 支持:O(1) | 不支持:O(N) |
头插 | 需要搬移元素,效率低:O(N) | 只需要修改引用的指向,时间复杂度为O(1) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |