上一篇文章中對劍指 offer
中數組相關的題目進行了概括,這一篇文章是鏈表篇。一樣地,若是各位大佬發現程序有什麼 bug
或其餘更巧妙的思路,歡迎交流學習。java
題目描述node
輸入一個鏈表的頭節點,從尾到頭打印鏈表的每一個節點的值。算法
這裏能夠用顯式棧,或者遞歸來實現,都比較簡單,也就很少作解釋了。數組
遞歸實現bash
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if(listNode == null){
return new ArrayList<>();
}
ArrayList<Integer> list = printListFromTailToHead(listNode.next);
list.add(listNode.val);
return list;
}
複製代碼
棧實現dom
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<Integer>();
if(listNode == null){
return list;
}
Deque<Integer> stack = new LinkedList<>();
ListNode node = listNode;
while(node != null) {
stack.push(node.val);
node = node.next;
}
while(!stack.isEmpty()) {
list.add(stack.pop());
}
return list;
}
複製代碼
題目一描述oop
在 O(1) 時間內刪除鏈表指定節點。給定單鏈表的頭節點引用和一個節點引用,要求在 O(1) 時間內刪除該節點。學習
解題思路ui
通常來講,要在單向鏈表中刪除指定節點,須要獲得被刪除節點的前驅節點。但這須要從頭節點開始順序查找,時間複雜度確定不是 O(1)
了,因此須要換一種思路。spa
咱們能夠將後繼節點的值賦值給要刪除的指定節點,再刪除下一個節點,如此也一樣實現了刪除指定節點的功能。可是還須要注意兩種特殊狀況:
代碼實現
public Node deleteNode(Node head, Node node) {
if (head == null || node == null) {
return head;
}
if (head == node) {
// 要刪除的節點是頭節點
return head.next;
} else if (node.next == null) {
// 要刪除的節點是尾節點
Node cur = head;
while (cur.next != node) {
cur = cur.next;
}
cur.next = null;
} else {
// 要刪除的節點在鏈表中間
ListNode nextNode = node.next;
node.val = nextNode.val;
node.next = nextNode.next;
}
return head;
}
複製代碼
這裏除了最後一個節點,其餘節點均可以在 O(1)
時間內刪除,只有要刪除的節點是尾節點時,才須要對鏈表進行遍歷,因此,整體的時間複雜度仍是 O(1)
。
題目二描述
在一個排序的鏈表中,存在重複的結點,請刪除該鏈表中重複的結點,重複的結點不保留,返回鏈表頭指針。 例如,鏈表1->2->3->3->4->4->5 處理後爲 1->2->5。
解題思路
這裏要刪除排序鏈表中的重複節點,因爲頭節點也可能被刪除,因此須要對頭節點特殊處理,或者添加一個虛擬節點。這裏選擇使用虛擬節點。
因爲這裏須要判斷當前節點和下一個節點的值,因此循環中條件就是要判斷當前節點和下一個節點均不能爲空。若是這兩個值不相等,則繼續遍歷。
若是不相等,則循環判斷跳過連續重複的數個節點,最後 cur
指向這些重複節點的最後一個。因爲重複節點不保留,因此須要讓 pre.next
指向 cur.next
,再更新 cur
爲下一個節點 pre.next
,進而繼續判斷。
代碼實現
public Node deleteDuplication(Node head) {
Node dummyHead = new Node(-1);
dummyHead.next = head;
Node pre = dummyHead;
Node cur = head;
while (cur != null && cur.next != null) {
if (cur.value != cur.next.value) {
pre = cur;
cur = cur.next;
} else {
while (cur.next != null && cur.value == cur.next.value) {
cur = cur.next;
}
pre.next = cur.next;
cur = pre.next;
}
}
return dummyHead.next;
}
複製代碼
這裏雖然有兩層嵌套循環,但實際上只對鏈表遍歷了一遍,因此其時間複雜度爲 O(n)
。另外只申請了一個虛擬節點,因此空間複雜度爲 O(1)
。
題目描述
輸入一個鏈表,輸出該鏈表中倒數第 k 個結點。(k 從 1 開始)
解題思路
這裏能夠定義兩個指針。第一個指針從鏈表頭開始遍歷,向前移動 k - 1
步。而後從 k
步開始,第二個指針也開始從鏈表頭開始遍歷。
因爲兩個指針的距離爲 k - 1
,全部當第一個指針移動到鏈表的尾節點時,第二個指針正好移動到倒數第 k
個節點。
代碼實現
public static ListNode findKthToTail(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
for (int i = 0; i < k - 1; i++) {
if (fast.next == null) {
return null;
}
fast = fast.next;
}
ListNode slow = head;
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
複製代碼
題目描述
給一個鏈表,若其中包含環,請找出該鏈表的環的入口結點,不然,輸出null。
解題思路
首先須要判斷鏈表是否有環,可使用兩個指針,同時從鏈表的頭部開始遍歷,一個指針一次走一步,一個指針一次走兩步。若是快指針能追上慢指針,則表示鏈表有環;不然若是快指針走到了鏈表的末尾,表示沒有環。
在找到環以後,定義一個指針指向鏈表的頭節點,再選擇剛纔的慢指針從快慢指針的相遇節點開始,兩個指針同時以每次一步向前移動,它們相遇的節點就是鏈表的入口節點。
代碼實現
public ListNode EntryNodeOfLoop(ListNode pHead) {
if(pHead == null || pHead.next == null) {
return null;
}
ListNode slow = pHead.next;
ListNode fast = slow.next;
while(slow != fast) {
if(fast == null || fast.next == null) {
return null;
}
slow = slow.next;
fast = fast.next.next;
}
ListNode p = pHead;
while(slow != p) {
slow = slow.next;
p = p.next;
}
return slow;
}
複製代碼
題目描述
輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。
循環解決
思路以下圖:
循環代碼
public ListNode reverseList1(ListNode head) {
ListNode newHead = null;
ListNode cur = head;
ListNode nex;
while (cur != null) {
nex = cur.next;
cur.next = newHead;
newHead = cur;
// 記錄
cur = nex;
}
return newHead;
}
複製代碼
遞歸解決
遞歸代碼
public ListNode reverseList2(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList2(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
複製代碼
題目描述
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,固然咱們須要合成後的鏈表知足單調不減規則。
循環解題
在使用循環時,首先須要肯定新鏈表的頭節點,若是鏈表 first
的頭節點的值小於鏈表 second
的頭節點的值,那麼鏈表 first
的頭節點即是新鏈表的頭節點。
而後循環處理兩個鏈表中剩餘的節點,若是鏈表 first
中的節點的值小於鏈表 second
中的節點的值,則將鏈表 first
中的節點添加到新鏈表的尾部,不然添加鏈表 second
中的節點。而後繼續循環判斷,直到某一條鏈表爲空。
當其中一條鏈表爲空後,只須要將另外一條鏈表所有連接到新鏈表的尾部。
思路圖以下:
循環代碼
public ListNode merge1(ListNode first, ListNode second) {
if (first == null) {
return second;
}
if (second == null) {
return first;
}
ListNode p = first;
ListNode q = second;
ListNode newHead;
if (p.val < q.val) {
newHead = p;
p = p.next;
} else {
newHead = q;
q = q.next;
}
ListNode r = newHead;
while (p != null && q != null) {
if (p.val < q.val) {
r.next = p;
p = p.next;
} else {
r.next = q;
q = q.next;
}
r = r.next;
}
if (p == null) {
r.next = q;
} else {
r.next = p;
}
return newHead;
}
複製代碼
遞歸解題
使用遞歸解決,比較簡單。首先判斷兩條鏈表是否爲空,若是 first
爲空,則直接返回 second
;若是 second
爲空,則直接返回 first
。
接着判斷鏈表 first
中節點的值和鏈表 second
中節點的值,若是 first
中節點的值較小,則遞歸地求 first.next
和 second
的合併鏈表,讓 first.next
指向新的鏈表頭節點,而後返回 first
便可。
另外一種狀況相似,這裏就再也不贅述了。
遞歸代碼
public ListNode merge2(ListNode first, ListNode second) {
if (first == null) {
return second;
}
if (second == null) {
return first;
}
if (first.val < second.val) {
first.next = merge2(first.next, second);
return first;
} else {
second.next = merge2(first, second.next);
return second;
}
}
複製代碼
題目描述
輸入一個複雜鏈表(每一個節點中有節點值,以及兩個指針,一個指向下一個節點,另外一個特殊指針指向任意一個節點),返回結果爲複製後複雜鏈表的head。
解題思路
這能夠分爲三步來解決。第一步是根據原始鏈表的全部節點,將每一節點的複製節點連接到它的後面。
第二步設置複製出來的節點的特殊指針。若是原始鏈表的節點 p
的特殊指針指向節點 s
,則複製出來的節點 cloned
的特殊指針就指向節點 s
的下一個節點。
第三部是將長鏈表拆分紅兩個鏈表,把全部偶數位置的節點鏈接起來就是新的複製出來的鏈表。
代碼實現
public RandomListNode Clone(RandomListNode head) {
cloneNodes(head);
connectSiblingNode(head);
return reconnectNodes(head);
}
private void cloneNodes(RandomListNode head) {
RandomListNode p = head;
while(p != null) {
RandomListNode newNode = new RandomListNode(p.label);
newNode.next = p.next;
p.next = newNode;
p = newNode.next;
}
}
private void connectSiblingNode(RandomListNode head) {
RandomListNode p = head;
while(p != null) {
RandomListNode cloned = p.next;
if(p.random != null) {
cloned.random = p.random.next;
}
p = cloned.next;
}
}
private RandomListNode reconnectNodes(RandomListNode head) {
RandomListNode p = head;
RandomListNode newHead = null;
RandomListNode tail = null;
if(p != null) {
tail = newHead = p.next;
p.next = tail.next;
p = p.next;
}
while(p != null) {
tail.next = p.next;
tail = tail.next;
p.next = tail.next;
p = p.next;
}
return newHead;
}
複製代碼
題目描述
輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能建立任何新的結點,只能調整樹中結點指針的指向。
解題思路
這裏將二叉搜索樹轉換爲一個排序的雙向鏈表,能夠採用使用遞歸算法。
首先遞歸地轉換左子樹,返回其鏈表頭節點,而後須要遍歷該鏈表,找到鏈表的尾節點,這是爲了和根節點相鏈接。須要讓鏈表的尾節點的 right
指向根節點,讓根節點的 left
指向鏈表的尾節點。
而後遞歸地轉換右子樹,返回其鏈表頭節點,而後須要讓根節點的 right
指向鏈表頭節點,讓鏈表的頭節點指向根節點。
最後判斷若是左子樹轉換的鏈表爲空,則返回以 root
根節點爲頭節點的鏈表,不然返回以左子樹最小值爲頭節點的鏈表。
代碼實現
public TreeNode Convert(TreeNode root) {
if(root == null) {
return null;
}
TreeNode leftHead = Convert(root.left);
TreeNode leftEnd = leftHead;
while(leftEnd != null && leftEnd.right != null) {
leftEnd = leftEnd.right;
}
if(leftEnd != null) {
leftEnd.right = root;
root.left = leftEnd;
}
TreeNode rightHead = Convert(root.right);
if(rightHead != null) {
root.right = rightHead;
rightHead.left = root;
}
return leftHead == null ? root : leftHead;
}
複製代碼
題目描述
輸入兩個鏈表,找出它們的第一個公共結點。
解題思路
對於兩個鏈表,若是有公共節點,要不它們就是同一條鏈表,要不它們的公共節點必定在公共鏈表的尾部。
能夠遍歷兩個鏈表獲得它們的長度,而後在較長的鏈表上,先走它們的長度差的步數,接着同時在兩個鏈表上遍歷,如此找到的第一個節點就是它們的第一個公共節點。
代碼實現
public ListNode findFirstCommonNode(ListNode first, ListNode second) {
int length1 = getListLength(first);
int length2 = getListLength(second);
ListNode headLongList = first;
ListNode headShortList = second;
int diff = length1 - length2;
if (length1 < length2) {
headLongList = second;
headShortList = first;
diff = length2 - length1;
}
for (int i = 0; i < diff; i++) {
headLongList = headLongList.next;
}
while (headLongList != null && headShortList != null) {
if (headLongList == headShortList) {
return headLongList;
}
headLongList = headLongList.next;
headShortList = headShortList.next;
}
return null;
}
public int getListLength(ListNode head) {
int length = 0;
ListNode cur = head;
while (cur != null) {
length++;
cur = cur.next;
}
return length;
}
複製代碼