劍指offer(連結串列)
03 從尾到頭答應連結串列
題目描述
輸入一個連結串列,按連結串列從尾到頭的順序返回一個ArrayList。
示例1:
輸入
{67,0,24,58}
返回值
[58,24,0,67]
題解:
利用棧先入後出的特性
程式碼:
import java.util.ArrayList; import java.util.Stack; public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> res = new ArrayList<>(); Stack<ListNode> stack=new Stack<ListNode>(); ListNode temp = listNode; while(temp != null){ stack.push(temp); temp = temp.next; } while(!stack.isEmpty()){ res.add(stack.pop().val); } return res; } }
複雜度
- 時間複雜度: O(n)
- 空間複雜度: O(n)
14 連結串列中倒數第k個節點
題目描述
輸入一個連結串列,輸出該連結串列中倒數第k個結點。
示例1
輸入
1,{1,2,3,4,5}
返回值
{5}
題解
使用如圖的快慢指標,首先讓快指標先行k步,然後讓快慢指標每次同行一步,直到快指標指向空節點,慢指標就是倒數第K個節點。
程式碼
public class Solution { public ListNode FindKthToTail(ListNode head,int k) { if(head == null || k == 0) return null; ListNode tem = head; ListNode res = head; for(int i = 1;i <= k;i++){ if(tem == null) return null; tem = tem.next; } while(tem != null){ tem = tem.next; res = res.next; } return res; } }
複雜度
- 時間複雜度:O(N)
- 空間複雜度:O(1)
15 反轉連結串列
題目描述
輸入一個連結串列,反轉連結串列後,輸出新連結串列的表頭。
示例1
輸入
{1,2,3}
返回值
{3,2,1}
題解:
雙指標遍歷連結串列
程式碼:
public class Solution { public ListNode ReverseList(ListNode head) { if(head == null) return head; ListNode pre = null; ListNode res = head; while(res != null){ ListNode temp = res.next; res.next = pre; pre = res; res = temp; } return pre; } }
複雜度
- 時間複雜度:O(n), 遍歷一次連結串列
- 空間複雜度:O(1)
16 合併兩個有序的連結串列
題解
遞迴法:
程式碼
//非遞迴法
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
ListNode newHead = new ListNode(-1);
ListNode current = newHead;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
// 跳出while迴圈後,list1或list2還沒有遍歷完成;分情況討論
if (list1 != null) current.next = list1;
if (list2 != null) current.next = list2;
return newHead.next;
}
}
//遞迴版
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null) return list2;
if(list2 == null) return list1;
ListNode res = null;
if(list1.val < list2.val){
res = list1;
res.next = Merge(list1.next,list2);
}else{
res = list2;
res.next = Merge(list1,list2.next);
}
return res;
}
}
複雜度分析:
- 時間複雜度: O(M+N)
- 空間複雜度: O(1) 節點引用 dumdum , cur 使用常數大小的額外空間。
25 複雜連結串列的複製
題目的描述
請實現 copyRandomList 函式,複製一個複雜連結串列。在複雜連結串列中,每個節點除了有一個 next 指標指向下一個節點,還有一個 random 指標指向連結串列中的任意節點或者 null。
輸入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
輸出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
題解
雜湊表:
利用雜湊表的查詢特點,考慮構建 原連結串列節點 和 新連結串列對應節點 的鍵值對對映關係,再遍歷構建新連結串列各節點的 next 和 random 引用指向即可。
演算法流程:
- 若頭節點 head 為空節點,直接返回 nullnull ;
- 初始化: 雜湊表 map , 節點 cur 指向頭節點;
- 遍歷連結串列:
- 建立新節點,並向 map 新增鍵值對 (原 cur 節點, 新 cur 節點) ;
- cur 遍歷至原連結串列下一節點;
- 構建新連結串列的引用指向,遍歷連結串列:
- 構建新節點的 next 和 random 引用指向;
- cur 遍歷至原連結串列下一節點;
- 返回值: 新連結串列的頭節點 map[cur] ;
程式碼
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;
Map<Node, Node> map = new HashMap<>();
// 3. 複製各節點,並建立 “原節點 -> 新節點” 的 Map 對映
while(cur != null) {
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
// 4. 構建新連結串列的 next 和 random 指向
while(cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
// 5. 返回新連結串列的頭節點
return map.get(head);
}
}
複雜度分析:
- 時間複雜度 O(N) : 兩輪遍歷連結串列,使用 O(N) 時間。
- 空間複雜度 O(N) : 雜湊表 map 使用線性大小的額外空間。
36 兩個連結串列的第一個公共節點
題解
- 浪漫相遇
我們使用兩個指標 p1,p2 分別指向兩個連結串列 pHeadA,pHeadB 的頭結點,然後同時分別逐結點遍歷,當 p1 到達連結串列 PheadA 的末尾時,重新定位到連結串列 pHeadB 的頭結點;當 p2 到達連結串列 pHeadB 的末尾時,重新定位到連結串列 pHeadA 的頭結點。
這樣,當它們相遇時,所指向的結點就是第一個公共結點。 - 利用HashSet的
程式碼
//浪漫相遇
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while(p1 != p2){
p1 = p1 == null ? pHead2 : p1.next;
p2 = p2 == null ? pHead1 : p2.next;
}
return p1;
}
}
複雜度
- 浪漫相遇
- 時間複雜度:O(M+N)
- 空間複雜度:O(1)
- Set
複雜度分析
- 時間複雜度:O(M+N)
- 空間複雜度:O(1)
55 連結串列中環的入口節點
題目描述
給一個連結串列,若其中包含環,請找出該連結串列的環的入口結點,否則,輸出null。
題解
利用HashSet的不重複性質
程式碼
import java.util.HashSet;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead){
if(pHead == null) return null;
ListNode cur = pHead;
HashSet<ListNode> set = new HashSet<>();
while(cur != null){
if(!set.contains(cur)){
set.add(cur);
}else{
return cur;
}
cur = cur.next;
}
return null;
}
}
複雜度
- 時間複雜度:O(N)
- 空間複雜度: O(N)
56 刪除連結串列中重複的節點
題目描述
在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,返回連結串列頭指標。
- 重複的結點不保留
連結串列1->2->3->3->4->4->5 處理後為 1->2->3->4->5
2.重複的結點保留
連結串列1->2->3->3->4->4->5 處理後為 1->2->5
題解
1.重複的結點不保留
這是一個簡單的問題,僅測試你操作列表的結點指標的能力。由於輸入的列表已排序,因此我們可以通過將結點的值與它之後的結點進行比較來確定它是否為重複結點。如果它是重複的,我們更改當前結點的 next 指標,以便它跳過下一個結點並直接指向下一個結點之後的結點。
- 重複的節點保留
這裡我們使用雙指標的方式,定義a,b兩個指標。
考慮到一些邊界條件,比如1->1->1->2這種情況,需要把開頭的幾個1給去掉,我們增加一個啞結點,方便邊界處理。
- 初始的兩個指標如下:
- 將a指標指向啞結點
- 將b指標指向head(啞結點的下一個節點)
2.遍歷連結串列 - 如果a指向的值不等於b指向的值,則兩個指標都前進一位
- 否則,就單獨移動b,b不斷往前走,直到a指向的值不等於b指向的值。
注意,這裡不是直接比較a.val==b.val
,這麼比較不對,因為初始的時候,a指向的是啞結點,所以比較邏輯應該是這樣:
a.next.val == b.next.val
當兩個指標指向的值相等時,b不斷往前移動,這裡是通過一個while迴圈判斷的,因為要過濾掉1->2->2->2->3重複的2。
那麼整個邏輯就是兩個while
程式碼
//1. 重複的節點不保留
public ListNode deleteDuplicates(ListNode head) {
ListNode current = head;
while (current != null && current.next != null) {
if (current.next.val == current.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}
//2. 重複的節點保留
import java.util.HashSet;
public class Solution {
public ListNode deleteDuplication(ListNode pHead){
if(pHead == null || pHead.next == null ) return pHead;
ListNode head = new ListNode(0);
head.next = pHead;
ListNode a = head, b = pHead;
while(b != null && b.next != null) {
if(a.next.val != b.next.val){
a = a.next;
b = b.next;
}else{
while(b != null && b.next != null && a.next.val == b.next.val){
b = b.next;
}
a.next = b.next;
b = b.next;
}
}
return head.next;
}
複雜度分析
- 保留重複的節點
- 時間複雜度:O(n),因為列表中的每個結點都檢查一次以確定它是否重複,所以總執行時間為 O(n),其中 n 是列表中的結點數。
- 空間複雜度:O(1),沒有使用額外的空間。
- 不保留重複的節點
- 時間複雜度:O(n)
- 空間複雜度:O(1)