鏈表
目錄
一、概述
1.鏈表是什么
鏈表數一種線性數據結構。它是動態地進行儲存分配的一種結構。
什么是線性結構,什么是非線性結構?
線性結構是一個有序數據元素的集合。常用的線性結構有:線性表,棧,隊列,雙隊列,數組,串。
非線性結構,是一個結點元素可能有多個直接前趨和多個直接后繼。常見的非線性結構有:二維數組,多維數組,廣義表,樹(二叉樹等)。
2.鏈表的基本結構
鏈表由一系列節點組成的集合,節點(Node)由數據域(date)和指針域(next)組成。
date負責儲存數據,next儲存其直接后續的地址
3.鏈表的分類
-
單鏈表(特點:連接方向都是單向的,對鏈表的訪問要通過順序讀取從頭部開始)
-
雙鏈表
- 循環鏈表
- 單向循環鏈表
- 雙向循環鏈表
4.鏈表和數組的比較
數組:
優點:查詢快(地址是連續的)
缺點:1.增刪慢,消耗CPU內存
鏈表就是 一種可以用多少空間就申請多少空間,並且提高增刪速度的線性數據結構,但是它地址不是連續的查詢慢。
二、單鏈表
[1. 認識單鏈表](#1. 認識單鏈表)
1. 認識單鏈表
(1)頭結點:第0 個節點(虛擬出來的)稱為頭結點(head),它沒有數據,存放着第一個節點的首地址
(2)首節點:第一個節點稱為首節點,它存放着第一個有效的數據
(3)中間節點:首節點和接下來的每一個節點都是同一種結構類型:由數據域(date)和指針域(next)組成
- 數據域(date)存放着實際的數據,如學號(id)、姓名(name)、性別(sex)、年齡(age)、成績(score)等
- 指針域(next)存放着下一個節點的首地址
(4)尾節點:最后一個節點稱為尾節點,它存放着最后一個有效的數據
(5)頭指針:指向頭結點的指針
(6)尾指針:指向尾節點的指針
(7)單鏈表節點的定義
public static class Node {
//Object類對象可以接收一切數據類型解決了數據統一問題
public Object date; //每個節點的數據
Node next; //每個節點指向下一結點的連接
public Node(Object date) {
this.date = date;
}
}
2.引人頭結點的作用
-
概念
頭結點:虛擬出來的一個節點,不保存數據。頭結點的next指針指向首節點。頭結點不是鏈表所必須的。
頭指針:指向鏈表第一個節點的指針。頭指針是鏈表所必須的
注:頭指針始終指向鏈表的第一個節點。 對於引入頭結點的鏈表:頭指針指向頭結點;對於沒有引入頭結點的鏈表:頭指針指向首節點。
-
為什么要引入頭結點
(1)對鏈表的刪除、插入操作時,第一個節點的操作更方便
如果沒有頭結點,頭指針指向鏈表的首節點,在首節點前插入一個新的節點時,頭指針要相應地指向新插入的節點。把首節點刪除時,頭結點的指向也要更新。
如果沒有頭結點,那我們在對首節點進行操作時,要一直維護着頭結點指向的更新。
如果引入了頭結點,頭指針始終指向頭結點。頭結點的next指針始終指向首節點
(2)統一空表和非空表的處理
引入頭指針后,頭指針指向頭結點,無論鏈表是否為空,頭指針均不為空。
3.鏈表的基本操作
(1)增加節點
在鏈表后增加節點
思路:
-
產生一個新的節點 newNode
-
對鏈表進行遍歷操作,找到當前鏈表的最后一個節點 last
-
當前鏈表的最后一個節點的下一個節點 = 新的節點 last.next = newNode
public Object add(Object obj){
//產生一個新的節點
Node newNode = new Node(obj);
//如果沒有任何節點存在(第一個節點)
if (size == 0){
head = newNode;
last = newNode;
}else { //如果不是第一個節點
last.next = newNode;
last = newNode;
}
size++;
return obj;
}
(2)插入結點
思路:
在指定位置插入新節點 nodeIndex ,新節點的前一個結點 current
- 遍歷到需要插入新節點 nodeIndex 的位置
- 當找到該位置時,新插入的結點下一結點 = 前一個結點的下一結點 nodeIndex.next = current.next
- 前一個結點的下一結點 = 新插入的結點 current.next = nodeIndex
public void addIndex(int index,double n){
Node current = head;
while (current != null){
if (current.date.equals(index)){
//產生一個新節點
Node nodeIndex = new Node(n);
nodeIndex.next = current.next;
current.next = nodeIndex;
size++;
}
current = current.next;
}
}
(3)刪除結點
思路:
-
定義一個需要刪除的結點 deleteNode
-
找到需要刪除的結點的前一個結點 previous
-
前一個結點 的下一個節點 = 需要刪除的結點 的下一個節點 previous.next = deleteNode.next
public boolean delete(Object value){
//鏈表為空
if (size == 0){
return false;
}
Node deleteNode = head; //要刪除的結點
Node previous = head; //要刪除的結點前一個結點
//沒找到要刪除的結點
while(deleteNode.date!= value){
if(deleteNode.next == null){
return false;
}else{
previous = deleteNode;
deleteNode = deleteNode.next;
}
}
//如果要刪除的是首節點
if (deleteNode.date == head.date){
head = head.next;
size--;
}else { //如果要刪除的是首節點之后的結點
previous.next = deleteNode.next;
size--;
}
return true;
}
(4)查找結點
思路:
- 因為頭結點不能動,定義一個當前節點 current,從頭結點開始遍歷
- 找到該節點返回 current ,找不到返回 null
public Node find(Object obj){
Node current = head;
int tempSize = size;
while (tempSize > 0){
if (obj.equals(current.date)){
return current;
}else {
current = current.next;
}
tempSize--;
}
return null;
}
(5)修改結點
思路:
修改指定節點的數據
- 遍歷到需要修改的結點
- 將節點數據進行替換
public void update(int map , int n){
if (size == 0){
System.out.println("鏈表為空");
return;
}
Node current = head;
for (int i = 1; i < map; i++) {
if (current.next == null){
System.out.println("該節點不存在");
break;
}
current = current.next;
if (i == map -1){
current.date = n;
}
}
}
5.設計鏈表:源代碼(含測試用例)
public class LinkedList {
private int size; //鏈表節點的個數
private Node head; //頭結點
private Node last; //當前鏈表的最后一個節點
public LinkedList(){
size = 0;
head = null;
}
//鏈表的每個節點類
public static class Node {
//Object類對象可以接收一切數據類型解決了數據統一問題
public Object date; //每個節點的數據
Node next; //每個節點指向下一結點的引用
public Node(Object date) {
this.date = date;
}
}
//在鏈表后添加元素
public Object add(Object obj){
//產生一個新的節點
Node newNode = new Node(obj);
//如果沒有任何節點存在(第一個節點)
if (size == 0){
head = newNode;
last = newNode;
}else { //如果不是第一個節點
last.next = newNode;
last = newNode;
}
size++;
return obj;
}
//插入結點
public void addIndex(int index,double n){
Node current = head;
while (current != null){
if (current.date.equals(index)){
//產生一個新節點
Node nodeIndex = new Node(n);
nodeIndex.next = current.next;
current.next = nodeIndex;
size++;
}
current = current.next;
}
}
//刪除(指定元素刪除節點)
public boolean delete(Object value){
//鏈表為空
if (size == 0){
return false;
}
Node deleteNode = head; //要刪除的結點
Node previous = head; //要刪除的結點前一個結點
//沒找到要刪除的結點
while(deleteNode.date!= value){
if(deleteNode.next == null){
return false;
}else{
previous = deleteNode;
deleteNode = deleteNode.next;
}
}
//如果要刪除的是首節點
if (deleteNode.date == head.date){
head = head.next;
size--;
}else { //如果要刪除的是首節點之后的結點
previous.next = deleteNode.next;
size--;
}
return true;
}
//查找指定元素的結點
public Node find(Object obj){
Node current = head;
int tempSize = size;
while (tempSize > 0){
if (obj.equals(current.date)){
return current;
}else {
current = current.next;
}
tempSize--;
}
return null;
}
//修改
public void update(int map , int n){
if (size == 0){
System.out.println("鏈表為空");
return;
}
Node current = head;
for (int i = 1; i < map; i++) {
if (current.next == null){
System.out.println("該節點不存在");
break;
}
current = current.next;
if (i == map -1){
current.date = n;
}
}
}
//顯示節點信息
public void display(){
if (size > 0){
Node node = head;
int tempSize = size;
while (tempSize > 0){
System.out.print(node.date+" ");
node = node.next;
tempSize--;
}
}else {
System.out.println("鏈表為空");
}
System.out.println();
}
}
測試用例
import javax.xml.soap.Node;
public class Application {
public static void main(String[] args) {
LinkedList list = new LinkedList();
System.out.println("在鏈表后添加節點:" );
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.display();
System.out.println("刪除第四個節點:" );
list.delete(4);
list.display();
System.out.println("查找數據是3的結點:" );
LinkedList.Node nodefind = list.find(3);
System.out.println(nodefind.date);
System.out.println("在第三節點后面增加一個節點:" );
list.addIndex(3,4);
list.display();
System.out.println("把第四個節點的4.0該成0:");
list.update(4,0);
list.display();
}
}
運行結果
三、雙鏈表
1.認識雙鏈表
雙鏈表的每個數據節點中都有兩個指針,分別前驅指針域和后繼指針域。
2.雙鏈表結點結構的定義
雙向鏈表中每個節點包含兩個節點的指針引用,和一個數據域
public static class Node{
private Object date;
private Node next; //指向下一結點的引用
private Node prev; //指向前一結點的引用
public Node(Object date){
this.date = date;
}
}
3.雙鏈表的基本操作
插入結點圖解
代碼實現
package DLinkendList;
public class LinkedList {
public static class Node{
private Object date;
private Node next; //指向下一結點的引用
private Node prev; //指向前一結點的引用
public Node(Object date){
this.date = date;
}
}
private Node head; //頭結點
private Node tail; //尾節點
private Node curr; //臨時結點,用作指針節點
private int size; //鏈表節點數
public void LinkedList(){
head = new Node(null);
tail = head;
size = 0;
}
//判斷鏈表是否為空
public boolean isEmpty(){
return size == 0;
}
//在鏈表尾部添加節點
public void add(Object obj){
if (isEmpty()){ //鏈表為空,添加第一個新節點
head = new Node(obj);
tail = head;
size++;
}else {
curr = new Node(obj);
curr.prev = tail;
tail.next = curr; //將新結點與原來的尾部結點連接
tail = curr; //curr變成最后一個節點
size++;
}
}
//插入結點
public void addIndex(int index,int value){
curr = head;
while (curr != null){
if (curr.date.equals(index)){
Node nodeIndex = new Node(value);
nodeIndex.prev = curr;
nodeIndex.next = curr.next;
curr.next = nodeIndex;
if (nodeIndex.next == null){
tail = nodeIndex;
}
size++;
}
curr = curr.next;
}
}
//刪除指定元素的結點
public boolean delete(Object value){
curr = head;
//鏈表為空
if (size == 0){
return false;
}
//沒找到要刪除的結點
while(curr.date!= value){
if(curr.next == null){
return false;
}else{
curr.prev = curr;
curr = curr.next;
}
}
//如果要刪除的是首節點
if (curr.date == head.date){
head = head.next;
size--;
}else { //如果要刪除的是首節點之后的結點
curr.prev.next = curr.next;
size--;
}
return true;
}
//打印鏈表
public void display(){
curr = head;
for (int i = 0; i < size; i++) {
System.out.print(curr.date + " ");
curr = curr.next;
}
System.out.println();
}
}
測試鏈表
public class Application {
public static void main(String[] args) {
LinkedList list = new LinkedList();
System.out.println("在鏈表后添加節點:" );
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.display();
System.out.println("在5后面加一個6:" );
list.addIndex(5,6);
list.display();
System.out.println("刪除元素6" );
list.delet(6);
list.display();
}
}
運行結果
四、雙指針
對於單鏈表,如果我們想在尾部添加一個節點,就必須從首節點開始遍歷到尾節點,
然后在尾結點后面插入一個節點。為了方便操作,可以在設計鏈表的時候多一個對尾結點的引用。
雙指針和雙鏈表的區別
1.雙端鏈表的實現
package DoublePointLinkedList;
public class LinkedList {
private Node head; //頭結點
private Node tail; //尾節點
private int size ; //節點個數
private static class Node{
private Object date;
private Node next;
public Node(Object date){
this.date = date;
}
}
public LinkedList(){
head = null;
tail = null;
size = 0;
}
//增加節點(表尾)
public void addTail(Object obj){
Node newNode = new Node(obj);
if (size == 0){
head = newNode;
tail = newNode;
size++;
}else {
tail.next = newNode;
tail = newNode;
size++;
}
}
//增加節點(表頭)
public void addHead(Object obj){
Node node = new Node(obj);
if (size == 0){
head = node;
tail = node;
size++;
}else {
node.next = head;
head = node;
size++;
}
}
//刪除首結點
public boolean deleteHead(){
if (size == 0){
return false;
}
if (head.next == null){
head = null;
tail = null;
}else {
head = head.next;
}
size--;
return true;
}
//顯示鏈表
public void display(){
Node node = head;
for (int i = 0; i < size; i++) {
System.out.print(node.date + " ");
node = node.next;
}
System.out.println();
}
}
雙端鏈表測試
package DoublePointLinkedList;
public class Application {
public static void main(String[] args) {
LinkedList list = new LinkedList();
System.out.println("在表尾添加節點:");
list.addTail(1);
list.addTail(2);
list.addTail(3);
list.addTail(4);
list.addTail(5);
list.addTail(6);
list.addTail(7);
list.display();
System.out.println("在表頭添加一個節點數據為0:");
list.addHead(0);
list.display();
System.out.println("刪除第一個結點:");
list.deleteHead();
list.display();
}
}
運行結果
2.環形鏈表
(1)環形鏈表就是循環鏈表的意思。循環鏈表沒有專門的頭結點,鏈表尾結點的指針域不指向null,而是指向鏈表的其他結點
(2)循環鏈表的實現
創建一個節點類Node
package CircularLinkendList;
public class Node {
private int data;
private Node next;
public Node(int data){
this.data = data;
}
public int getData() {
return data;
}
public Node getNext() {
return next;
}
public void setData(int data) {
this.data = data;
}
public void setNext(Node next) {
this.next = next;
}
}
寫一個循環鏈表添加節點的方法
思路:
(1)鏈表為空的時候,插入第一個節點
那插入的這個節點是第一個節點也是最后一個節點
也就是這個節點的next指向自己的地址
(2)插入第二個節點
實例化一個輔助指針 currentNode ,讓這個輔助指針指向第一個節點的地址
讓輔助指針 currentNode 的 next 指向新的節點 newNode (currentNode.next = newNode)
(3)把鏈表“環”起來
再實例化一個輔助指針 first ,這個輔助指針也指向第一個節點的地址
讓新節點 newNode 的next指向第一個節點,也就是指向first(newNode.next = first)
思路清晰之后上代碼
創建一個鏈表類
package CircularLinkendList;
public class LinkendList {
private Node first = null;
private Node currentNone = null;
public void add(int value){
for (int i = 1; i <= value; i++) {
Node newNode = new Node(i);
if (first == null){
first = newNode;
first.setNext(first);
currentNone = first;
}else {
currentNone.setNext(newNode);
newNode.setNext(first);
currentNone = currentNone.getNext();
}
}
}
//顯示鏈表
public void display(){
Node node = first;
if (node == null){
System.out.println("鏈表為空");
return;
}do {
System.out.print(node.getData() + " ");
node = node.getNext();
}while (node != first);
System.out.println();
}
}
測試
package CircularLinkendList;
public class Application {
public static void main(String[] args) {
LinkendList list = new LinkendList();
list.add(5);
list.display();
}
}
運行結果
3.判斷鏈表中是否有環
問題描述
給定一個鏈表,判斷鏈表中是否有環
如果存在環,則返回true, 否則返回 false
為了給定鏈表中的環,用整數 pos 來表示;鏈表尾連接到鏈表中的位置(索引從0 開始),如果 pos 是 -1 ,則在該鏈表中沒有環( pos 是為了標識鏈表的實際情況)。
示例1:
輸入:head = [3 , 2 , 0 , 4] , pos = 1;
輸出:ture
解釋:鏈表中有一個環,其尾部連接到第二個節點
示例2:
輸入:head = [1 , 2] , pos = 0;
輸出:ture
解釋:鏈表中有一個環,其尾部連接到第一個節點
示例3:
輸入:head = [1] , pos = -1;
輸出:false
解釋:鏈表中沒有環
代碼實現
public boolean hasLoop(Node node){
//定義一個快指針一個慢指針
Node slow = node;
Node fast = node.next;
while (fast != null){
if (slow.data == fast.data){ //當兩個指針重逢時,則存在環,否則不存在
return true;
}
slow = slow.next; //每次迭代慢指針走一步
fast = fast.next.next; //快指針走二步
if (fast == null){
return false;
}
}
return true; //只有一個元素也存在環
}
測試
public class Application {
public static void main(String[] args) {
LinkendList list = new LinkendList();
Node node1 = new Node(3);
Node node2 = new Node(2);
Node node3 = new Node(0);
Node node4 = new Node(4);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2;//構造一個帶環的鏈表(和 pos = 1 差不多意思)
System.out.println(list.hasLoop(node2));
}
}
運行結果
4.相交鏈表
問題描述
給兩個單鏈表的頭節點 headA
和 headB
,找出並返回兩個單鏈表相交的起始節點。如果兩個鏈表沒有交點,返回 null
。
題目數據 保證 整個鏈式結構中不存在環。
注意,函數返回結果后,鏈表必須 保持其原始結構 。
示例1:
輸入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
輸出:Intersected at '8'
解釋:相交節點的值為 8 (注意,如果兩個鏈表相交則不能為 0)。
從各自的表頭開始算起,鏈表 A 為 [4,1,8,4,5],鏈表 B 為 [5,0,1,8,4,5]。
在 A 中,相交節點前有 2 個節點;在 B 中,相交節點前有 3 個節點。
示例2:
輸入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
輸出:null
解釋:從各自的表頭開始算起,鏈表 A 為 [2,6,4],鏈表 B 為 [1,5]。
由於這兩個鏈表不相交,所以 intersectVal 必須為 0,而 skipA 和 skipB 可以是任意值。
這兩個鏈表不相交,因此返回 null 。
思路:
下面我們來分析示例1:
- 指針 NodeA 指向鏈表 ListA ,指針 Node 指向鏈表 ListA , 依次往后遍歷
-
NodeA 遍歷到 ListA 的末尾,則 NodeA = headB 繼續遍歷
-
NodeB 遍歷到 ListB 的末尾,則 NodeB = headA 繼續遍歷
- 此時兩鏈表的長度差就沒有了
- 繼續往下遍歷就能得到結果了
public LinkedList check(LinkedList headA, LinkedList headB) {
if (headA == null || headB == null)
return null;
LinkedList nodeA = headA;
LinkedList nodeB = headB;
while (nodeA != nodeB) {
nodeA = nodeA == null ? headB : nodeA.next;
nodeB = nodeB == null ? headA : nodeB.next;
}
return nodeA;
}
5.刪除倒數第N個節點
問題描述
給你一個鏈表,刪除鏈表的倒數第 n 個結點,並且返回鏈表的頭結點。
示例1:
輸入:head = [1,2,3,4,5], n = 2
輸出:[1,2,3,5]
示例2:
輸入:head = [1], n = 1
輸出:[]
輸入:head = [1,2], n = 1
輸出:[1]
思路:
- 設前指針 start ,后指針 end ,兩個指針都指向 head
-
移動 start ,使start 和 end 相距 n
-
start 和 end 同時先前移動,直到 start 指向 null,此時 end 的位置恰好是倒數第 n 個節點的前一個結點
-
end 的 next 指向 下一個節點的 next的 next (end.next = end.next.next)
代碼實現
public class deleteNLinkedList {
public ListNode removeNthFromEnd(ListNode head,int n){
ListNode pre = new ListNode(0); //pre:虛擬指針
pre.next = head;
ListNode start = pre;
ListNode end = pre;
while (n != 0){ // start 先走 n 步
start = start.next;
n--;
}
while (start.next != null){ //start 和 end 相距 n 時一起移動
start = start.next;
end = end.next;
}
end.next = end.next.next; //刪除第倒數第 n 個節點
return pre.next;
}
}
五、經典問題—反轉鏈表
問題描述
給你單鏈表的頭節點 head ,請你反轉鏈表,並返回反轉后的鏈表。
示例
輸入:head = [1,2,3,4,5]
輸出:[5,4,3,2,1]
思路
將當前節點的 \textit{next}next 指針改為指向前一個節點
代碼實現
public class LinkendList {
public Node reverse(Node head){
Node prev = null;
Node curr = head;
while (curr != null){
Node tempNode = curr.next;
curr.next = prev;
prev = curr;
curr = tempNode;
}
return prev;
}
}
運行結果
了解單鏈表https://www.bilibili.com/video/BV16K4y1N7WL
思路https://blog.csdn.net/weixin_43265151/article/details/104659700