Note
- 題解彙總:劍指offer題解彙總
- 代碼地址:Github 劍指offer Java實現彙總
- 點擊目錄中的題名鏈接可直接食用題解~
- 有些解法博文中未實現,不代表一定很難,可能只是因爲博主太懶```(Orz)
- 如果博文中有明顯錯誤或者某些題目有更加優雅的解法請指出,謝謝~
目錄
題號 | 題目名稱 |
---|---|
11 | 二進制中1的個數 |
12 | 數值的整數次方 |
13 | 調整數組順序使奇數位於偶數前面 |
14 | 鏈表中倒數第k個結點 |
15 | 反轉鏈表 |
16 | 合併兩個排序的鏈表 |
17 | 樹的子結構 |
18 | 二叉樹的鏡像 |
19 | 順時針打印矩陣 |
20 | 包含min函數的棧 |
正文
11、二進制中1的個數
題目描述
輸入一個整數,輸出該數二進制表示中1的個數。其中負數用補碼錶示。
題目分析
解法一: 首先需要了解一個前提:將數字n和1做&運算,可以判斷n當前最後一位是否是1。有了這個前提後,我們就可以不斷將數字n進行無符號右移,將n和1做&操作判斷當前數字最後一位是否爲1並計數即可。
使用無符號右移>>>而不是帶符號右移>>的原因:因爲對於帶符號右移,正數右移高位補0,負數右移高位補1;而對於無符號右移,無論是正數還是負數,高位通通補0。如果選擇帶符號右移,當n爲負數時,會一直在n的左邊補1,n永遠不會等於0,程序會進入死循環。
代碼實現
解法一: O(1)
public int NumberOf1(int n) {
int count = 0;
while (n != 0) {
count += n & 1;
n >>>= 1;
}
return count;
}
12、數值的整數次方
題目描述
給定一個double類型的浮點數base和int類型的整數exponent。求base的exponent次方。保證base和exponent不同時爲0。
題目分析
解法一: 使用單層循環對底數base進行累乘即可,但是需要考慮邊界0的情況,以及當指數exponent爲負數時,結果應該爲(base的exponent次方)分之1。
解法二: 使用快速冪的方式進行計算,即x的n次方乘以x的n次方等於x的2n次方,由此二分計算結果。
代碼實現
解法一: O(n)
public double Power(double base, int exponent) {
if (base == 0) return 0;
if (exponent == 0) return 1;
double res = 1;
for (int i = 1; i <= Math.abs(exponent); i++) {
res *= base;
}
return exponent > 0 ? res : 1 / res;
}
解法二: O(logn)
public double Power(double base, int exponent) {
long N = n;
return n >= 0 ? quickCal(x, N) : 1 / quickCal(x, -N);
}
public double quickCal(double x, long n) {
double res = 1.0;
double tx = x;
while (n > 0) {
if ((n & 1) == 1) {
res *= tx;
}
tx *= tx;
n /= 2;
}
return res;
}
13、調整數組順序使奇數位於偶數前面
題目描述
輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。
題目分析
解法一: 開闢一個新數組,對數組進行兩次遍歷,第一次遍歷尋找奇數並存放在新數組中,第二次遍歷尋找偶數並存放在新數組中。該解法的空間複雜度爲O(n)。
解法二: 本解法參考快排中的partition思想,不開闢新數組,可以將奇數放在偶數前面,但由於partition方法的不穩定性,無法保證數字之間的相對次序不變,時間複雜度爲O(n),空間複雜度爲O(1)。如果需要滿足題目中所述的“奇數和奇數,偶數和偶數之間的相對位置不變”,就需要通過數組移位實現swap的穩定性,由於需要循環移位,故最終的時間複雜度會退化成O(n²)。
代碼實現
解法一: O(n)
public void reOrderArray(int [] array) {
int[] newArr = new int[array.length];
int index = 0;
for (int i = 0; i < array.length; i++) {
if ((array[i] & 1) == 1) {
newArr[index++] = array[i];
}
}
for (int i = 0; i < array.length; i++) {
if ((array[i] & 1) == 0) {
newArr[index++] = array[i];
}
}
for (int i = 0; i < array.length; i++) {
array[i] = newArr[i];
}
}
解法二: O(n²)
public void reOrderArray1(int [] array) {
int pre = -1;
int suf = array.length;
int l = 0;
while (l < suf) {
if ((array[l] & 1) == 1) {
swap(array, ++pre, l++);
} else {
swap(array, --suf, l);
}
}
}
14、鏈表中倒數第k個結點
題目描述
輸入一個鏈表,輸出該鏈表中倒數第k個結點。
題目分析
解法一: 雙指針法。使用快指針和慢指針兩個指針,快指針先走k步,然後快慢指針同時走,當快指針指向null時,慢指針所指結點即爲鏈表中倒數第k個結點。
代碼實現
解法一: O(n)
public ListNode FindKthToTail(ListNode head, int k) {
if (head == null) return null;
ListNode fast = head;
ListNode slow = head;
while (k-- > 0) {
if (fast != null) {
fast = fast.next;
} else {
return null;
}
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
15、反轉鏈表
題目描述
輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。
題目分析
解法一: 使用pre和next兩個指針,分別標識當前節點的前驅和後繼,對指針指向進行改變即可。
解法二: 使用遞歸實現,對當前節點的next進行遞歸,只需要考慮返回得到的是指向一個末尾節點的指針,然後對指針進行調整即可。
代碼實現
解法一: O(n)
public ListNode ReverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode pre = null;
ListNode next = null;
while (head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
解法二: O(n)
public ListNode ReverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode last = ReverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
16、合併兩個排序的鏈表
題目描述
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。
題目分析
解法一: 設置頭結點head和頭結點的引用cur。當兩個鏈表都不爲空時,cur指向兩個鏈表指向節點中較小的那一個,然後指針後移;當有一個鏈表爲空時,將cur直接指向非空鏈表的後續頭節點。該過程其實就是歸併排序中的merge過程。
代碼實現
解法一: O(min(m,n))
public ListNode Merge(ListNode list1, ListNode list2) {
ListNode head = new ListNode(-1);
ListNode cur = head;
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;
}
if (list1 != null) {
cur.next = list1;
}
if (list2 != null) {
cur.next = list2;
}
return head.next;
}
17、樹的子結構
題目描述
輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)
題目分析
解法一: 非遞歸實現。
1、實現isSubTree()方法,傳入兩個值相同的節點t1和t2,用於判斷以t2爲根節點的樹是否是以t1爲根節點的樹的子結構;
2、在Main方法中對樹A進行遍歷,尋找與樹B根節點值相同的節點,調用isSubTree()判斷是否存在子結構;
3、本解法未使用遞歸實現,而是實現了兩種非遞歸的遍歷,isSubTree()中採用層序遍歷,主方法中採用中序遍歷。
解法二: 遞歸實現。
1、由於判斷的是tree2是否爲tree1的子結構,所以當tree2爲空時,tree1無論爲什麼值,都應該返回true;
2、當tree1爲空,tree2不爲空時,說明tree2不是tree1的子結構,返回false;
3、當tree1和tree2的當前值不等時,返回false;
4、當tree1和tree2都不爲空且值相等時,說明當前節點判斷完畢,接下來需要對當前節點的左、右子樹進行遞歸判斷。
代碼實現
解法一: O(m*n)
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
if (root2 == null) return false;
Stack<TreeNode> stack = new Stack<>();
while (!stack.empty() || root1 != null) {
if (root1 != null) {
stack.push(root1);
root1 = root1.left;
} else {
root1 = stack.pop();
if (root1.val == root2.val && isSubTree(root1, root2)) {
return true;
}
root1 = root1.right;
}
}
return false;
}
public static boolean isSubTree(TreeNode root1, TreeNode root2) {
Queue<TreeNode> q1 = new LinkedList<>();
Queue<TreeNode> q2 = new LinkedList<>();
q1.add(root1);
q2.add(root2);
while (!q2.isEmpty()) {
TreeNode t1 = q1.poll();
TreeNode t2 = q2.poll();
if (t1.val != t2.val) {
return false;
} else {
if (t1.left != null && t2.left != null) {
q1.add(t1.left);
q2.add(t2.left);
} else if (t1.left == null && t2.left != null){
return false;
}
if (t1.right != null && t2.right != null) {
q1.add(t1.right);
q2.add(t2.right);
} else if (t1.right == null && t2.right != null){
return false;
}
}
}
return true;
}
解法二: O(m*n)
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
if (root1 == null || root2 == null) return false;
return isSubTree(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
}
public boolean isSubTree(TreeNode tree1, TreeNode tree2){
if (tree2 == null) return true;
if (tree1 == null) return false;
if (tree1.val != tree2.val) return false;
return isSubTree(tree1.left, tree2.left) && isSubTree(tree1.right, tree2.right);
}
18、二叉樹的鏡像
題目描述
操作給定的二叉樹,將其變換爲源二叉樹的鏡像。
題目分析
解法一: 非遞歸實現。對該樹進行層次遍歷,對當前遍歷的節點進行判斷:
1、如果左右孩子都不爲空,則交換兩個孩子,並將兩個節點入隊;
2、如果左右孩子有一個爲空,則交換兩個孩子,也就是將非空孩子賦給空節點,對非空孩子置空,將交換後的非空節點入隊;
3、如果左右子樹都爲空,則無需處理。
解法二: 遞歸實現。將當前遍歷節點的左右孩子進行交換,然後遞歸地對左右孩子進行相同操作,當前節點爲null時結束遞歸。
代碼實現
解法一: O(n)
public void Mirror(TreeNode root) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
root = queue.poll();
if (root.left != null && root.right != null) {
TreeNode t = root.left;
root.left = root.right;
root.right = t;
queue.add(root.left);
queue.add(root.right);
} else if (root.left != null && root.right == null) {
root.right = root.left;
root.left = null;
queue.add(root.right);
} else if (root.left == null && root.right != null) {
root.left = root.right;
root.right = null;
queue.add(root.left);
}
}
}
解法二: O(n)
public void Mirror(TreeNode root) {
if(root == null) return;
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
Mirror(root.left);
Mirror(root.right);
}
19、順時針打印矩陣
題目描述
輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每一個數字,例如,如果輸入如下4 X 4矩陣: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 則依次打印出數字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
題目分析
解法一: 該類題目需要從宏觀的角度去考慮。
1、首先定義一個方法printLevel,給定矩陣的左上角和右下角兩個點,使用四個循環遍歷順時針輸出矩陣的外圈。注:需要考慮只包含一行和一列的情況,當只有一行時,從左往右依次打印;當只有一列時,從上往下依次打印。
2、確定矩陣的左上角和右下角,不斷縮小矩陣,遞進到內圈矩陣,調用步驟1中的方法打印當前矩陣的最外圈。
代碼實現
解法一: O(m*n)
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> list = new ArrayList<Integer>();
int row1 = 0;
int col1 = 0;
int row2 = matrix.length - 1;
int col2 = matrix[0].length - 1;
while (row1 <= row2 && col1 <= col2) {
printLevel(list, matrix, row1++, col1++, row2--, col2--);
}
return list;
}
public static void printLevel(ArrayList<Integer> list, int[][] matrix,
int row1, int col1, int row2, int col2) {
if (row1 == row2) {
for (int i = col1; i <= col2; i++) {
list.add(matrix[row1][i]);
}
} else if (col1 == col2){
for (int i = row1; i <= row2; i++) {
list.add(matrix[i][col1]);
}
} else {
int curRow = row1;
int curCol = col1;
while (curCol < col2) {
list.add(matrix[curRow][curCol++]);
}
while (curRow < row2) {
list.add(matrix[curRow++][curCol]);
}
while (curCol > col1) {
list.add(matrix[curRow][curCol--]);
}
while (curRow > row1) {
list.add(matrix[curRow--][curCol]);
}
}
}
20、包含min函數的棧
題目描述
定義棧的數據結構,請在該類型中實現一個能夠得到棧中所含最小元素的min函數(時間複雜度應爲O(1))。
題目分析
解法一: 雙棧法。使用min棧來存儲當前棧的最小元素,對於最小元素相同的連續序列,在min棧中只存儲一個,可以壓縮部分空間,儘管空間複雜度都爲O(n)。
1、當壓入棧時,如果min棧爲空則直接壓入;否則判斷當前壓入元素是否小於等於min棧的棧頂元素:如果小於等於則說明當前壓入的元素是stack的最小元素,則push到min棧,如果大於則說明min棧的棧頂元素依然是stack中的最小元素,則不做任何操作;
2、當彈出棧時,如果stack和min棧的棧頂元素相同,說明stack中的最小元素此時要出棧了,則min棧也執行pop操作。
代碼實現
解法一: O(n)
Stack<Integer> stack = new Stack<>();
Stack<Integer> minStack = new Stack<>();
public void push(int node) {
stack.push(node);
if (minStack.empty()) {
minStack.push(node);
} else {
if (node <= minStack.peek()) {
minStack.push(node);
}
}
}
public void pop() {
if (stack.peek() == minStack.peek()) {
minStack.pop();
}
stack.pop();
}
public int top() {
return stack.peek();
}
public int min() {
return minStack.peek();
}