系列文章目錄
數據結構之ArrayList-CSDN博客
目錄
系列文章目錄
前言
一、模擬實現鏈表
1. 遍歷鏈表
2. 插入節點
?3. 刪除節點
4. 清空鏈表
二、鏈表的常見操作
1. 反轉鏈表
2. 返回鏈表的中間節點
3. 鏈表倒數第 k 個節點
4. 合并兩個有序鏈表
5. 分割鏈表
6. 判斷鏈表是否回文
7. 找兩個鏈表的第一個公共節點
8. 判斷鏈表是否有環
9. 尋找鏈表入環的第一個節點
三、模擬實現雙向鏈表
1. 遍歷鏈表
2. 插入節點
3. 刪除節點
4. 清空鏈表
四、ArrayList 和 LinkedList 的區別
前言
本文通過單鏈表的模擬實現,幫助了解單鏈表的底層原理,以及常見的鏈表的常用操作,比如反轉鏈表,找鏈表的中間節點等。模擬實現雙向鏈表,了解雙向鏈表的底層原理,以及 ArrayList 和 LinkedList 的比較。
一、模擬實現鏈表
鏈表是物理存儲結構上非連續,邏輯上連續的存儲結構;每個節點都會保存下一個節點的哈希值,通過這個節點可以找到下一個節點;
鏈表的屬性:用 head 表示鏈表的頭節點;
public class MySingleList {static class ListNode{public int val;public ListNode next;public ListNode(int val){this.val = val;}}public ListNode head;// 鏈表的方法// ...
}
1. 遍歷鏈表
以下方法都需要遍歷一遍鏈表:
display(): void 打印節點信息;
size(): int 返回鏈表的長度;
contains(int key): boolean 判斷鏈表中是否包含 key;
// 打印所有節點信息public void display(){ListNode cur = head;while(cur != null){System.out.print(cur.val + " ");cur = cur.next;}}// 求鏈表的長度public int size(){ListNode cur = head;int count = 0;while(cur != null){count++;cur = cur.next;}return count;}// 查找關鍵字 key 是否在鏈表中public boolean contains(int key){ListNode cur = head;while(cur != null){if(cur.val == key){return true;}cur = cur.next;}return false;}
2. 插入節點
addFirst(int data): void 頭插法,將節點插入到鏈表頭節點的位置;
addLast(int data): void 尾插法,將節點插入到鏈表尾節點的位置,尾插的時候要注意,當鏈表沒有節點,即 head == null,這時候要注意將要插入的節點設置為頭節點;
findIndexPrev(int index): ListNode 找到 index 位置節點的前驅;
addIndex(int index, int data): void 在 index 位置插入節點;
插入之前需要先判斷 index 位置是否合法;
如果合法再判斷一下是否是頭插,是否是尾插,因為這倆方法已經寫好了,可以調用;
找到要插入節點的前驅,找到前驅之后進行插入;
// 頭插法public void addFirst(int data){ListNode node = new ListNode(data);node.next = head;head = node;}// 尾插法public void addLast(int data){ListNode node = new ListNode(data);ListNode cur = head;if(cur == null) {this.head = node;return;}while(cur.next != null){cur = cur.next;}cur.next = node;}// 在任意位置插入public void addIndex(int index, int data){if(index < 0 || index > size()){throw new PosOutOfBoundsException(index + "插入位置不合法");}if(index == 0){addFirst(data);return;}if(index == size()){addLast(data);return;}ListNode cur = findIndexPrev(index);ListNode node = new ListNode(data);node.next = cur.next;cur.next = node;}// 找到 index 下標的前一個節點private ListNode findIndexPrev(int index){ListNode cur = head;while(--index > 0){cur = cur.next;}return cur;}
?3. 刪除節點
remove(int key): void 刪除第一個值為 key? 的節點;
如果鏈表為空則直接返回;
如果頭節點為要刪除的節點,直接刪除;
如果要刪除的節點不存在,拋異常;
如果要刪除在后面,先找到要刪除節點的前驅,再刪除;
findPrev(int key): ListNode 找到值為 key 的節點的前驅,沒有要刪除的節點,返回 null;
removeAll(int key): void 刪除所有值為 key 的節點;
如果鏈表為空,直接返回;
從頭節點后面開始刪除,找到要刪除的節點和它的前驅,進行刪除;
刪除完成后,當前節點指針向后移動;
如果沒有找到要刪除的節點,前驅和當前節點指針都同步向后移動;
最后判斷頭節點,如果頭節點也是要刪除的節點,進行刪除;
// 刪除節點public void remove(int key){if(head == null){return;}// 單獨刪除頭節點if(head.val == key){head = head.next;return;}ListNode prev = findPrev(key);if(prev == null){throw new RuntimeException("要刪除的節點不存在!");}ListNode del = prev.next;prev.next = del.next;}// 找到 data 節點的前驅private ListNode findPrev(int key){ListNode cur = head;while(cur.next != null){if(cur.next.val == key){return cur;}}return null;}// 刪除所有值為 key 的節點public void removeAll(int key){if(head == null){return;}ListNode prev = head;ListNode cur = prev.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;}}
4. 清空鏈表
clear(): void 清空鏈表,只需要將頭結點置空即可;
// 清空鏈表public void clear(){this.head = null;}
二、鏈表的常見操作
1. 反轉鏈表
reverseList(ListNode head): ListNode 反轉鏈表;
如果鏈表為空,返回 null;
如果鏈表不為空,依次將 head 后面的元素進行頭插;
public ListNode reverseList(ListNode head) {if(head == null) return head;ListNode cur = head.next;head.next = null;while(cur != null){ListNode next = cur.next;cur.next = head;head = cur;cur = next;}return head;}
2. 返回鏈表的中間節點
middleNode(ListNode head): ListNode 返回鏈表的中間節點,如果鏈表是奇數個元素,返回中間節點,如果鏈表是偶數個元素,返回右中點節點;
定義快慢指針,快指針每次走兩步,慢指針每次走一步,如果快指針走到最后一個位置或者空位置,返回慢指針指向的節點即可;
public ListNode middleNode(ListNode head) {if(head == null){return head;}ListNode fast = head;ListNode slow = head;while(fast != null && fast.next != null){fast = fast.next;fast = fast.next;slow = slow.next;}return slow;}
3. 鏈表倒數第 k 個節點
findKthtoTail(ListNode head, int k): ListNode 找倒數第 k 個節點
先判斷 head 是否為空,如果 head 為空,返回 null;
再判斷 k 的值是否合法,如果不合法,返回 null;
定義快慢指針 fast 和 slow,先讓 fast 走 k - 1步,再讓 fast 和 slow 同時走, fast 走到尾巴節點的時候,slow 正好走到倒數第 k 個節點;?
// 返回鏈表倒數第 k 個節點public ListNode findKthToTail(ListNode head, int k){if(head == null) return null;if(k <= 0 || k > size()) return null;ListNode fast = head;ListNode slow = head;while(--k > 0){fast = fast.next;}while(fast.next != null){fast = fast.next;slow = slow.next;}return slow;}
4. 合并兩個有序鏈表
mergeTwoLists(ListNode list1, ListNode list2): ListNode 合并兩個有序鏈表
兩個鏈表的元素相互比較,小的加入新的隊列,指針后移;
當有一個鏈表為空了,開始把剩下的鏈表的元素按順序加入到新的鏈表中;
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {if(list1 == null) return list2;if(list2 == null) return list1;ListNode newHead = new ListNode();ListNode cur = newHead;while(list1 != null && list2 != null){if(list1.val <= list2.val){cur.next = list1;list1 = list1.next;}else{cur.next = list2;list2 = list2.next;}cur = cur.next;}while(list1 != null){cur.next = list1;list1 = list1.next;cur = cur.next;}while(list2 != null){cur.next = list2;list2 = list2.next;cur = cur.next;}return newHead.next;}
5. 分割鏈表
要求:將小于 x 的節點排在其余節點之前,不改變原來節點的順序;
partition(ListNode pHead, int x): ListNode 分割鏈表;
小于 x 的節點放 head1 后面,大于等于 x 的節點放在 head2 后面;
合并 head1 和 head2,返回新的鏈表;
注意:后面的鏈表的尾巴節點的 next 一定要置空,避免成環;
public ListNode partition(ListNode pHead, int x) {if(pHead == null) return null;ListNode head1 = new ListNode(0);ListNode head2 = new ListNode(0);ListNode cur1 = head1;ListNode cur2 = head2;ListNode cur = pHead;while(cur != null){if(cur.val < x){cur1.next = cur;cur1 = cur1.next;}else{cur2.next = cur;cur2 = cur2.next;}cur = cur.next;}// 這是需要注意的地方cur2.next = null;cur1.next = head2.next;return head1.next;}
6. 判斷鏈表是否回文
chkPalindrom(ListNode A): boolean 檢查鏈表是否回文;
reverse(ListNode head): ListNode 反轉鏈表;
檢查鏈表是否回文的關鍵首先是找到鏈表中點(奇數個節點找中點,偶數個節點找右中點),找到中點后從這個節點開始,逆置鏈表,形成兩個鏈表;
分別從兩個鏈表的頭結點開始遍歷鏈表,如果值不同,直接返回 false,如果值相同繼續遍歷,直到遍歷完,返回 true;
public boolean chkPalindrome(ListNode A) {if (A == null) return true;ListNode fast = A;ListNode slow = A;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;}ListNode cur1 = A;ListNode cur2 = reverse(slow);boolean flag = true;while (cur1.next != null && cur2.next != null) {if (cur1.val != cur2.val) {flag = false;break;}cur1 = cur1.next;cur2 = cur2.next;}return flag;}public ListNode reverse(ListNode head) {ListNode newHead = new ListNode(0);ListNode cur = head;while (cur != null) {ListNode next = cur.next;cur.next = newHead.next;newHead.next = cur;cur = next;}return newHead.next;}
7. 找兩個鏈表的第一個公共節點
getIntersectionNode(ListNode headA, ListNode headB): ListNode 找兩個鏈表的第一個公共節點,沒有公共節點返回 null;
定義兩個指針遍歷兩個鏈表,同時讓兩個指針都往后走,如果相同返回節點即可;
如果不相同,當第一個指針遍歷到最后,就讓第一個指針指向第二個鏈表的頭結點;同理,第二個指針邊歷到最后,讓第二個指針也指向第一個鏈表;
這時候兩個指針都同時向后移動,如果兩個鏈表有公共節點,則一定會同時到達,因為兩個指針速度相同,相同時間走的路程也一定相同;
如果兩個鏈表沒有公共節點,則最終會同時指向 null,返回 null 即可;
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {if(headA == null || headB == null) return null;ListNode cur1 = headA;ListNode cur2 = headB;while(cur1 != cur2){if(cur1 != null) cur1 = cur1.next;else cur1 = headB;if(cur2 != null) cur2 = cur2.next;else cur2 = headA;}return cur1;}
8. 判斷鏈表是否有環
hasCycle(ListNode head): boolean 判斷鏈表是否有環;
定義一個 fast,一個 slow 指針,每次 fast 走兩步,slow 每次走一步,如果兩個指針相遇了,就證明有環,否則就沒有環;
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;}
9. 尋找鏈表入環的第一個節點
detectCycle(ListNode head): ListNode 尋找鏈表入環的第一個節點;
先找到 fast 和 slow 的相遇點,一個從起點出發,另一個從相遇點出發,最終兩個指針會在環的入口點相遇;
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;slow = head;while(fast != slow){fast = fast.next;slow = slow.next;}return fast;}
三、模擬實現雙向鏈表
雙向鏈表的屬性:用 head 表示鏈表的頭節點,last 表示鏈表的尾巴節點;
public class MyLinkedList {static class ListNode{private int val;private ListNode prev;private ListNode next;public ListNode(int data){this.val = data;}}public ListNode head;public ListNode last;
}
1. 遍歷鏈表
display(): void 打印鏈表;
size(): int 返回鏈表長度;
contains(int key): boolean 判斷鏈表中是否包含 key;
// 打印鏈表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 count = 0;while(cur != null){count++;cur = cur.next;}return count;}// 判斷鏈表中是否包含 keypublic boolean contains(int key) {ListNode cur = head;while (cur != null) {if (cur.val == key) {return true;}cur = cur.next;}return false;}
2. 插入節點
addFirst(int data): void 頭插法,將節點插入到鏈表頭節點的位置;
注意:如果鏈表沒有節點,插入的時候要把頭指針和尾指針都指向插入的節點;
addLast(int data): void 尾插法,將節點插入到鏈表尾節點的位置,尾插的時候要
注意:如果鏈表沒有節點,插入的時候要把頭指針和尾指針都指向插入的節點;
findIndexPrev(int index): ListNode 找到 index 位置節點的前驅;
addIndex(int index, int data): void 在 index 位置插入節點;
插入之前需要先判斷 index 位置是否合法;
如果合法再判斷一下是否是頭插,是否是尾插,因為這倆方法已經寫好了,可以調用;
找到要插入節點的前驅和后繼,進行插入;
// 頭插法public void addFirst(int data){ListNode node = new ListNode(data);if(head == null) {head = node;last = node;}else {node.next = head;head.prev = node;head = node;}}// 尾插法public void addLast(int data){ListNode node = new ListNode(data);if(head == null){head = node;last = node;}else{last.next = node;node.prev = last;last = node;}}// 在 index 位置插入public void addIndex(int index, int data){if(index < 0 || index > size()){throw new RuntimeException(index + "位置不合法!");}if(index == 0){addFirst(data);return;}else if(index == size()){addLast(data);return;}ListNode node = new ListNode(data);ListNode prevNode = findPrev(index);ListNode nextNode = prevNode.next;prevNode.next = node;node.prev = prevNode;node.next = nextNode;nextNode.prev = node;}private ListNode findPrev(int index){ListNode cur = head;while(--index > 0){cur = cur.next;}return cur;}
3. 刪除節點
remove(int key): void 刪除第一個值為 key? 的節點;
找到要刪除節點的前驅和后繼;
如果前驅為空,就刪除頭節點,刪除頭結點要特殊處理只有一個節點的情況;
如果后繼為空,就刪除尾節點,刪除尾節點要特殊處理只有一個節點的情況;
如果刪除中間節點,刪除后返回;
removeAll(int key): void 刪除所有值為 key 的節點;
注意:原理同上,不同點是刪除一個節點后,繼續向后遍歷;
// 刪除第一次出現關鍵字 key 的節點public void remove(int key){ListNode cur = head;while(cur != null){if(cur.val == key){ListNode prevNode = cur.prev;ListNode nextNode = cur.next;if(prevNode != null) {prevNode.next = nextNode;}else{head = head.next;if(head != null){head.prev = null;}}if(nextNode != null) {nextNode.prev = prevNode;}else{last = last.prev;if(last != null){last.next = null;}}break;}cur = cur.next;}}// 刪除所有值為 key 的節點public void removeAll(int key){ListNode cur = head;while(cur != null) {if (cur.val == key) {ListNode prevNode = cur.prev;ListNode nextNode = cur.next;if (prevNode != null) {prevNode.next = nextNode;} else {head = head.next;if(head != null){head.prev = null;}}if (nextNode != null) {nextNode.prev = prevNode;} else {last = last.prev;if(last != null){last.next = null;}}cur = nextNode;}else{cur = cur.next;}}}
4. 清空鏈表
clear(): void 清空鏈表;
注意:要把 head 和 last 置空;
public void clear(){ListNode cur = head;while(cur != null){ListNode next = cur.next;cur = null;cur = next;}head = null;last = null;}
四、ArrayList 和 LinkedList 的區別
存儲上:
ArrayList 在存儲空間上是連續的,LinkedList 邏輯上是,連續的,在存儲空間上是不一定連續的;
操作上:
從查詢元素上來說,ArrayList 的時間復雜度是 O(1),而 LinkedList 不支持隨機訪問;
從插入上來說:
LinkedList 的時間復雜度是 O(1),ArrayList 頭插時間復雜度是 O(n),因為需要把把元素統一往后移一個位置;
ArrayList 在插入時,空間不夠時,需要擴容,而 LInkedList 沒有容量的概念;
應用場景:
ArrayList 適合頻繁查詢,LinkedList 適合頻繁插入刪除;