棧、佇列、矩陣、連結串列問題(二)
目錄
- “之”字形列印
- 在行列都排好序的矩陣中找數
- 列印兩個有序連結串列的公共部分
- 判斷一個連結串列是否為迴文結構
- 將單向連結串列按某值劃分成左邊小、中間相等、右邊大的形式
- 複製含有隨機指標節點的連結串列
- 兩個單鏈表相交的一系列問題
“之”字形列印
【題目】 給定一個矩陣matrix,按照“之”字形的方式列印這個矩陣,例如: 1 2 3 4 5 6 7 8 9 10 11 12。“之”字形列印的結果為:1,2,5,9,6,3,4,7,10,11,8,12
【要求】 額外空間複雜度為O(1)。
思路:如果一直隨著“之”字軌跡計算下標,那演算法就非常繁瑣複雜,我們需要找到更簡潔的方式。這裡我們可以引入兩個輔助座標a,b。a向矩陣的右側移動,到最右側時就向下移動;b向下移動,到最底層時就向右移動。每次a和b只移動一步,並且交叉列印a和b的連線線經過的所有元素,直到a和b在最右下角重合。
public static void printMatrixZigZag(int[][] matrix) { int ay = 0; int ax = 0; int by = 0; int bx = 0; int endY = matrix.length - 1; int endX = matrix[0].length - 1; boolean fromUp = false; while (ay != endY + 1) { printLevel(matrix, ay, ax, by, bx, fromUp); ay = ax == endX ? ay + 1 : ay; ax = ax == endX ? ax : ax + 1; bx = by == endY ? bx + 1 : bx; by = by == endY ? by : by + 1; fromUp = !fromUp; } System.out.println(); } public static void printLevel(int[][] m, int ay, int ax, int by, int bx, boolean f) { if (f) { while (ay != by + 1) { System.out.print(m[ay++][ax--] + " "); } } else { while (by != ay - 1) { System.out.print(m[by--][bx++] + " "); } } }
在行列都排好序的矩陣中找數
【題目】 給定一個有N*M的整型矩陣matrix和一個整數K,matrix的每一行和每一列都是排好序的。實現一個函式,判斷K是否在matrix中。例如: 0 1 2 52 3 4 74 4 4 85 7 7 9。如果K為7,返回true;如果K為6,返回false。
【要求】 時間複雜度為O(N+M),額外空間複雜度為O(1)
思路:應該好好利用行列都排好序的這個特性。我們從第一行的最後一個數開始判斷,如果該數大於k,那麼繼續在該行中找,如果該數小於k,那麼換到下一行。
public static boolean isContains(int[][] matrix, int k){ int row = 0; int col = matrix[0].length - 1; while (row < matrix.length && col > -1){ if(matrix[row][col] == k){ return true; }else if(matrix[row][col] > k){ col--; }else{ row++; } } return false; }
列印兩個有序連結串列的公共部分
【題目】 給定兩個有序連結串列的頭指標head1和head2,列印兩個連結串列的公共部分。
思路:因為是有序連結串列,所有兩個連結串列的頭開始如下判斷:
- 如果head1的值小於head2,則head1向下移動
- 如果head1的值大於head2,則head2向下移動
- 如果head1等於haed2,則列印這個值,然後head1和head2都向下移動
public static class Node { public int value; public Node next; public Node(int data) { this.value = data; } } public static void printCommonPart(Node head1, Node head2) { while (head1 != null && head2 != null){ if(head1.value > head2.value){ head2 = head2.next; } else if(head1.value < head2.value){ head1 = head1.next; } else { System.out.print(head1.value); head1 = head1.next; head2 = head2.next; } } }
判斷一個連結串列是否為迴文結構
【題目】 給定一個連結串列的頭節點head,請判斷該連結串列是否為迴文結構。 例如: 1->2->1,返回true。 1->2->2->1,返回true。15->6->15,返回true。 1->2->3,返回false。
思路:我們可以使用一個棧結構將連結串列物件壓入其中,然後每彈出一個元素與連結串列的next元素比較。
public static boolean isPalindrome1(Node head) { Stack<Node> stack = new Stack<>(); Node cur = head; while (cur != null){ stack.push(cur); cur = cur.next; } while (head != null){ if(head.value == stack.pop().value){ return true; } } return false; }
進階: 如果連結串列長度為N,時間複雜度達到O(N),額外空間複雜度達到O(1)
思路:由於額外空間複雜度為O(1),那麼不能再使用棧的方式。
- 我們可以使用快慢指標法,設定一個快指標和一個慢指標,慢指標一次走一步,快指標一次走兩步,當快指標指向null,說明慢指標走到了連結串列中間位置。
- 反轉中間連結串列後面的指標。從頭尾向中間掃描,對比每個元素是否相等,如果都相等,則是迴文數,否則不是迴文數。
// need O(1) extra space public static boolean isPalindrome3(Node head) { if (head == null || head.next == null) { return true; } Node n1 = head; Node n2 = head; while (n2.next != null && n2.next.next != null) { // find mid node n1 = n1.next; // n1 -> mid n2 = n2.next.next; // n2 -> end } n2 = n1.next; // n2 -> right part first node n1.next = null; // mid.next -> null Node n3 = null; while (n2 != null) { // right part convert n3 = n2.next; // n3 -> save next node n2.next = n1; // next of right node convert n1 = n2; // n1 move n2 = n3; // n2 move } n3 = n1; // n3 -> save last node n2 = head;// n2 -> left first node boolean res = true; while (n1 != null && n2 != null) { // check palindrome if (n1.value != n2.value) { res = false; break; } n1 = n1.next; // left to mid n2 = n2.next; // right to mid } n1 = n3.next; n3.next = null; while (n1 != null) { // recover list n2 = n1.next; n1.next = n3; n3 = n1; n1 = n2; } return res; }
將單向連結串列按某值劃分成左邊小、中間相等、右邊大的形式
【題目】 給定一個單向連結串列的頭節點head,節點的值型別是整型,再給定一個整數pivot。實現一個調整連結串列的函式,將連結串列調整為左部分都是值小於pivot的節點,中間部分都是值等於pivot的節點,右部分都是值大於pivot的節點。
思路:可以建立一個輔助陣列,將連結串列的所有節點全部填入陣列中,按需求劃分好以後在拼裝回連結串列。
public static class Node { public int value; public Node next; public Node(int data) { this.value = data; } } public static Node listPartition1(Node head, int pivot) { if (head == null) { return head; } Node cur = head; int i = 0; while (cur.next != null) { i++; cur = cur.next; } Node[] nodeArr = new Node[i]; i = 0; cur = head; for (i = 0; i != nodeArr.length; i++) { nodeArr[i] = cur; cur = cur.next; } arrPartition(nodeArr, pivot); for (i = 1; i != nodeArr.length; i++) { nodeArr[i - 1].next = nodeArr[i]; } nodeArr[i - 1].next = null; return nodeArr[0]; } private static void arrPartition(Node[] nodeArr, int pivot) { int small = -1; int big = nodeArr.length; int index = 0; while (index != big) { if (nodeArr[index].value < pivot) { swap(nodeArr, ++small, index++); } else if (nodeArr[index].value == pivot) { index++; } else { swap(nodeArr, --big, index); } } } public static void swap(Node[] nodeArr, int a, int b) { Node tmp = nodeArr[a]; nodeArr[a] = nodeArr[b]; nodeArr[b] = tmp; }
進階:在原問題的要求之上再增加如下兩個要求。
- 在左、中、右三個部分的內部也做順序要求,要求每部分裡的節點從左到右的順序與原連結串列中節點的先後次序一致。
- 如果連結串列長度為N,時間複雜度請達到O(N),額外空間複雜度請達到O(1)。
思路:將連結串列分為3段:小於、等於、大於部分。每一部分組成一個連結串列,並且分別有兩個指標指向頭部和尾部,遍歷連結串列時按需求將每個節點分別接入各自的連結串列中,完成以後再組成一個完整連結串列,這樣就保證了連結串列結構的穩定性。
public static Node listPartition2(Node head, int pivot) { Node sH = null; // small head Node sT = null; // small tail Node eH = null; // equal head Node eT = null; // equal tail Node bH = null; // big head Node bT = null; // big tail Node next = null; // save next node // every node distributed to three lists while (head != null) { next = head.next; head.next = null; if (head.value < pivot) { if (sH == null) { sH = head; sT = head; } else { sT.next = head; sT = head; } } else if (head.value == pivot) { if (eH == null) { eH = head; eT = head; } else { eT.next = head; eT = head; } } else { if (bH == null) { bH = head; bT = head; } else { bT.next = head; bT = head; } } head = next; } // small and equal reconnect if (sT != null) { sT.next = eH; eT = eT == null ? sT : eT; } // all reconnect if (eT != null) { eT.next = bH; } return sH != null ? sH : eH != null ? eH : bH; }
複製含有隨機指標節點的連結串列
思路:使用HashMap結構,key為原連結串列的節點,value為複製的節點。將源節點對應的複製節點的每一個指標都指向原節點目標節點對應複製節點。
public static class Node { public int value; public Node next; public Node rand; public Node(int data) { this.value = data; } } public static Node copyListWithRand1(Node head) { HashMap<Node, Node> map = new HashMap<>(); Node cur = head; while (cur != null){ map.put(cur, new Node(cur.value)); cur = cur.next; } cur = head; while (cur != null){ map.get(cur).next = map.get(cur.next); map.get(cur).rand = map.get(cur.rand); cur = cur.next; } return map.get(head); }
進階:不使用額外的資料結構,只用有限幾個變數,且在時間複雜度為O(N)內完成原問題要實現的函式。
思路:
- 將每一個原節點next指標指向它的複製節點,複製節點next指標再指向下一個原節點。
- 再將複製節點的rand指標指向原節點rand指標指向的節點的複製節點。
- 最後將複製節點next指標指向下一個複製節點。
public static Node copyListWithRand2(Node head) { if (head == null) { return null; } Node cur = head; Node next = null; // copy node and link to every node while (cur != null) { next = cur.next; cur.next = new Node(cur.value); cur.next.next = next; cur = next; } cur = head; Node curCopy = null; // set copy node rand while (cur != null) { next = cur.next.next; curCopy = cur.next; curCopy.rand = cur.rand != null ? cur.rand.next : null; cur = next; } Node res = head.next; cur = head; // split while (cur != null) { next = cur.next.next; curCopy = cur.next; cur.next = next; curCopy.next = next != null ? next.next : null; cur = next; } return res; }
兩個單鏈表相交的一系列問題
【題目】 在本題中,單鏈表可能有環,也可能無環。給定兩個單鏈表的頭節點head1和head2,這兩個連結串列可能相交,也可能不相交。請實現一個函式,如果兩個連結串列相交,請返回相交的第一個節點;如果不相交,返回null即可。要求:如果連結串列1的長度為N,連結串列2的長度為M,時間複雜度請達到O(N+M),額外空間複雜度請達到O(1)。
思路:先判斷兩個連結串列是否有環,一個有環一個無環那麼必定不想交;兩個都無環或兩個都有環,則需要分開處理。
判斷連結串列是否有環,需要使用快指標、慢指標,快指標走兩步,慢指標走一步,當快指標和慢指標重合,快指標指向頭部,並且改為一次走一步。下一次快慢指標重合時,就是入環節點。
public static Node getIntersectNode(Node head1, Node head2) { if (head1 == null || head2 == null) { return null; } Node loop1 = getLoopNode(head1); Node loop2 = getLoopNode(head2); if (loop1 == null && loop2 == null) { //兩個都無環 return noLoop(head1, head2); } if (loop1 != null && loop2 != null) { //兩個都有環 return bothLoop(head1, loop1, head2, loop2); } return null; } //判斷是否有環 public static Node getLoopNode(Node head) { if (head == null || head.next == null || head.next.next == null) { return null; } Node n1 = head.next; // n1 -> slow Node n2 = head.next.next; // n2 -> fast while (n1 != n2) { if (n2.next == null || n2.next.next == null) { return null; } n2 = n2.next.next; n1 = n1.next; } n2 = head; // n2 -> walk again from head while (n1 != n2) { n1 = n1.next; n2 = n2.next; } return n1; }
若兩個都是無環連結串列,如果兩個連結串列尾部都沒有重合,則必定不相交;如果相交,那麼先得到兩個連結串列的長度差值n,然後長連結串列先走n步,再同步往下走,直到兩個連結串列的節點重合,即相交交點。
public static Node noLoop(Node head1, Node head2) { if (head1 == null || head2 == null) { return null; } Node cur1 = head1; Node cur2 = head2; int n = 0; while (cur1.next != null) { n++; cur1 = cur1.next; } while (cur2.next != null) { n--; cur2 = cur2.next; } if (cur1 != cur2) { return null; } cur1 = n > 0 ? head1 : head2; cur2 = cur1 == head1 ? head2 : head1; n = Math.abs(n); while (n != 0) { n--; cur1 = cur1.next; } while (cur1 != cur2) { cur1 = cur1.next; cur2 = cur2.next; } return cur1; }
若兩個都是有環連結串列,先判斷兩個連結串列的入環節點是否是一個節點,是一個節點則說明兩個連結串列共享一個環,必定相交,只需按無環連結串列的方式得到交點即可。如果入環節點不是一個節點,要麼不想交,要麼相交但入環點不在一個位置。此時只需在第一個連結串列的入環點向下尋找,如果找到第二個入環點,即相交;找不到就不想交。
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) { Node cur1 = null; Node cur2 = null; if (loop1 == loop2) { cur1 = head1; cur2 = head2; int n = 0; while (cur1 != loop1) { n++; cur1 = cur1.next; } while (cur2 != loop2) { n--; cur2 = cur2.next; } cur1 = n > 0 ? head1 : head2; cur2 = cur1 == head1 ? head2 : head1; n = Math.abs(n); while (n != 0) { n--; cur1 = cur1.next; } while (cur1 != cur2) { cur1 = cur1.next; cur2 = cur2.next; } return cur1; } else { cur1 = loop1.next; while (cur1 != loop1) { if (cur1 == loop2) { return loop1; } cur1 = cur1.next; } return null; } }
參考資料:牛客網左程雲初級演算法教程