Note
- 題解彙總:劍指offer題解彙總
- 代碼地址:Github 劍指offer Java實現彙總
- 點擊目錄中的題名鏈接可直接食用題解~
- 有些解法博文中未實現,不代表一定很難,可能只是因爲博主太懶```(Orz)
- 如果博文中有明顯錯誤或者某些題目有更加優雅的解法請指出,謝謝~
目錄
題號 | 題目名稱 |
---|---|
21 | 棧的壓入、彈出序列 |
22 | 從上往下打印二叉樹 |
23 | 二叉搜索樹的後序遍歷序列 |
24 | 二叉樹中和爲某一值的路徑 |
25 | 複雜鏈表的複製 |
26 | 二叉搜索樹與雙向鏈表 |
27 | 字符串的排列 |
28 | 數組中出現次數超過一半的數字 |
29 | 最小的K個數 |
30 | 連續子數組的最大和 |
正文
21、棧的壓入、彈出序列
題目描述
輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能爲該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。(注意:這兩個序列的長度是相等的)
題目分析
解法一: 設置一個指向pop數組的index指針,遍歷push數組,針對每一個遍歷值做如下操作:
1、將當前遍歷值壓入棧中;
2、查看棧頂元素和pop數組的當前彈出值是否相等,如果相等則模擬出棧操作,將stack的棧頂元素彈出,並對pop數組的指針進行後移。循環此過程直至兩元素不相等,說明棧頂元素彈出不滿足當前的pop序列;
3、如果最終棧空,則說明我們定義的棧成功模擬了pop數組的出棧順序,返回true;如果棧不爲空,則說明無法按照pop數組的順序出棧,返回false。
代碼實現
解法一: O(n)
public static boolean IsPopOrder(int [] pushA,int [] popA) {
Stack<Integer> stack = new Stack<>();
int index = 0;
for (int i = 0; i < pushA.length; i++) {
stack.push(pushA[i]);
while (!stack.isEmpty() && stack.peek() == popA[index]) {
index++;
stack.pop();
}
}
return stack.isEmpty();
}
22、從上往下打印二叉樹
題目描述
從上往下打印出二叉樹的每個節點,同層節點從左至右打印。
題目分析
解法一: 層序遍歷。層序遍歷其實就是廣度遍歷,使用隊列輔助實現;深度遍歷需要使用棧輔助實現。如果具體實現中,我們將返回類型定義爲ArrayList,就可以不需要開闢額外的隊列空間,直接用數組加index指針完成層序遍歷,將空間複雜度從O(n)優化爲O(1)。
代碼實現
解法一: O(n)
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> list = new ArrayList<>();
if (root == null) return list;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.add(root);
while(!queue.isEmpty()) {
root = queue.poll();
list.add(root.val);
if (root.left != null) {
queue.add(root.left);
}
if (root.right != null) {
queue.add(root.right);
}
}
return list;
}
23、二叉搜索樹的後序遍歷序列
題目描述
輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的數組的任意兩個數字都互不相同。
題目分析
解法一: 遞歸實現。我們需要判斷二叉搜索樹的後續遍歷序列,對於後序遍歷序列而言,序列的最後一個元素一定爲根節點。且對於二叉搜索樹而言,左子樹的元素一定比根節點小,右子樹的元素一定比根節點大。因此對於合法的二叉搜書的後續遍歷序列,可以先將當前序列的最後一個元素設置爲根節點,我們一定能夠在序列中找到一箇中點,中點的左半部分全部小於等於最後一個元素,中點的右半部分全部大於等於最後一個元素。如果找不到這個中點,則說明這是不合法的序列,返回false。最後遞歸地對中點劃分出的左右子樹進行同樣的求解,直到子數組元素小於等於2個說明無需再分,返回true。
解法二: 最大最小邊界約束法。只是看到有這種方法,暫未實現,據說可以達到O(n)的時間複雜度,感興趣可自行學習。
代碼實現
解法一: O(nlogn)
public boolean verify(int[] array, int begin, int end) {
if (end - begin < 2) return true;
int key = array[end];
int i;
for (i = begin; i < end; i++) {
if (array[i] > key) break;
}
for (int j = i + 1; j < end; j++) {
if (array[j] < key) return false;
}
return verify(array, begin, i - 1) && verify(array, i, end - 1);
}
24、二叉樹中和爲某一值的路徑
題目描述
輸入一顆二叉樹的根節點和一個整數,打印出二叉樹中結點值的和爲輸入整數的所有路徑。路徑定義爲從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。(注意: 在返回值的list中,數組長度大的數組靠前)
題目分析
解法一: 遞歸實現。根據前序遍歷的思想對樹進行遍歷,當到達葉子節點時且該條路徑的值符合條件時,將整條路徑添加到result中。注意,需要使用result.add(new ArrayList<>(list))而不能使用result.add(list),因爲list只是一個引用,result中的數據會被list後續的數據更新所影響。而使用result.add(new ArrayList<>(list))是將list重新拷貝了一份加入result中。在左右孩子的遞歸結束後,需要將list中在當前層次添加的節點移除,避免路徑重複。
解法二: 非遞歸實現。使用棧進行深度優先遍歷,暫未實現。
代碼實現
解法一: O(logn)
public ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
public ArrayList<Integer> list = new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
tranverse(root, target);
return result;
}
public void tranverse(TreeNode root, int target) {
if (root == null) {
return;
}
list.add(root.val);
if (target == root.val && root.left == null && root.right == null) {
result.add(new ArrayList<>(list));
}
FindPath(root.left, target - root.val);
FindPath(root.right, target - root.val);
list.remove(list.size() - 1);
return;
}
25、複雜鏈表的複製
題目描述
輸入一個複雜鏈表(每個節點中有節點值,以及兩個指針,一個指向下一個節點,另一個特殊指針指向任意一個節點),返回結果爲複製後複雜鏈表的head。(注意,輸出結果中請不要返回參數中的節點引用,否則判題程序會直接返回空)
題目分析
解法一: 在原鏈表的基礎上,在每個鏈表節點後new出一個label相同的新節點;然後重新遍歷鏈表,複製原節點上的random給新節點;最後將兩個鏈表拆開。
解法二: 使用HashMap存儲新舊節點的對應關係,再次遍歷鏈表取出相應的新節點。暫未實現。
代碼實現
解法一: O(n)
public static RandomListNode Clone(RandomListNode pHead) {
if (pHead == null) {
return null;
}
RandomListNode currentNode = pHead;
//1、複製每個結點,如複製結點A得到A1,將結點A1插到結點A後面;
while (currentNode != null){
RandomListNode cloneNode = new RandomListNode(currentNode.label);
RandomListNode nextNode = currentNode.next;
currentNode.next = cloneNode;
cloneNode.next = nextNode;
currentNode = nextNode;
}
currentNode = pHead;
//2、重新遍歷鏈表,複製老結點的隨機指針給新結點,如A1.random = A.random.next;
while (currentNode != null) {
currentNode.next.random = currentNode.random == null ? null : currentNode.random.next;
currentNode = currentNode.next.next;
}
//3、拆分鏈表,將鏈表拆分爲原鏈表和複製後的鏈表
currentNode = pHead;
RandomListNode pCloneHead = pHead.next;
while (currentNode != null) {
RandomListNode cloneNode = currentNode.next;
currentNode.next = cloneNode.next;
cloneNode.next = cloneNode.next == null ? null : cloneNode.next.next;
currentNode = currentNode.next;
}
return pCloneHead;
}
26、二叉搜索樹與雙向鏈表
題目描述
輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能創建任何新的結點,只能調整樹中結點指針的指向。
題目分析
解法一: 二叉搜索樹的中序遍歷就是有序序列,因此對二叉搜索樹進行中序遍歷,將中序遍歷的當前節點與前一個節點進行連接。本解法使用棧完成非遞歸的遍歷。
解法二: 遞歸實現二叉樹的中序遍歷,將中序遍歷的結果依次添加到數組中,然後遍歷整個數組,建立雙向鏈表。未實現。
代碼實現
解法一: O(n)
public static TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) {
return pRootOfTree;
}
TreeNode head = pRootOfTree;
TreeNode tmp = null;
Stack<TreeNode> stack = new Stack<>();
while (head != null || !stack.empty()) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
TreeNode pop = stack.pop();
head = pop;
if (tmp != null) {
tmp.right = pop;
}
pop.left = tmp;
tmp = pop;
head = head.right;
}
}
while (tmp.left != null) {
tmp = tmp.left;
}
return tmp;
}
27、字符串的排列
題目描述
輸入一個字符串,按字典序打印出該字符串中字符的所有排列。例如輸入字符串abc,則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。
輸出描述
輸入一個字符串,長度不超過9(可能有字符重複),字符只包括大小寫字母。
題目分析
解法一: 該題目其實就是對數組中的每個字符和其它字符進行交換完成的,但是該交換過程是遞歸完成的。以"abc"爲例,在第一層遞歸中,會將第一個字符’a’和後續字符進行依次交換,形成"abc"、"bac"和"cba"傳入下一層遞歸,在下一次遞歸中,又會分別以整個字符串的第二個字符開始,與後續字符依次交換。當交換到最後一個字符時,說明交換完畢,加入Set中,之所以使用Set是因爲題目要求結果是去重且按字典序排序的。
解法二: 該解法只保證最後結果是去重的,我們在交換的過程中加入了哈希表,當交換的兩個字符相同時,我們不進行交換操作,以保證結果不重複。但是無法保證結果按字典序排序。
代碼實現
解法一: O(n²)
public static ArrayList<String> Permutation(String str) {
if (str == null || str.length() == 0) {
return new ArrayList<>();
}
Set<String> set = new TreeSet<>();
helper(str.toCharArray(), 0, set);
return new ArrayList<>(set);
}
public static void helper(char[] s, int i, Set<String> set) {
if (i == s.length) {
set.add(String.valueOf(s));
}
for (int j = i; j < s.length; j++) {
swap(s, i, j);
helper(s, i + 1, set);
swap(s, i, j);
}
}
public static void swap(char[] s, int i, int j) {
if (i == j) {
return;
}
s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
}
解法二: O(n²)
public static void helper(char[] s, int i) {
if (i == s.length) {
System.out.println(String.valueOf(s));
}
HashSet<Character> set = new HashSet<>();
for (int j = i; j < s.length; j++) {
if (!set.contains(s[j])) {
set.add(s[j]);
swap(s, i, j);
helper(s, i + 1);
swap(s, i, j);
}
}
}
public static void swap(char[] s, int i, int j) {
if (i == j) {
return;
}
s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
}
28、數組中出現次數超過一半的數字
題目描述
數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。例如輸入一個長度爲9的數組{1,2,3,2,2,2,5,4,2}。由於數字2在數組中出現了5次,超過數組長度的一半,因此輸出2。如果不存在則輸出0。
題目分析
解法一: 遍歷數組,用哈希表記錄每個數字出現的次數,最後把記錄表遍歷一遍查看是否有大於數組一半的數字。
解法二: 用preValue記錄上一次訪問的值,count表明當前值出現的次數,如果下一個值和當前值相同那麼count++;如果不同count- -,減到0的時候就要更換新的preValue值了,因爲如果存在超過數組長度一半的值,那麼最後preValue一定會是該值。但是由於可能不存在該數,所以最後還需要重新遍歷一遍數組查看所選數是否真的符合條件,未必比解法一快。
代碼實現
解法一: O(n)
public static int MoreThanHalfNum_Solution(int [] array) {
if (array == null || array.length == 0) {
return 0;
}
int halfLen = array.length / 2;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < array.length; i++) {
map.put(array[i], map.getOrDefault(array[i], 0) + 1);
}
Iterator<Integer> it = map.keySet().iterator();
while (it.hasNext()) {
int t = it.next();
if (map.get(t) > halfLen) {
return t;
}
}
return 0;
}
解法二: O(n)
public static int MoreThanHalfNum_Solution(int[] array) {
if (array == null || array.length == 0) {
return 0;
}
int preValue = 0;
int count = 0;
for (int i = 0; i < array.length; i++) {
if (count == 0) {
preValue = array[i];
count = 1;
} else {
if (array[i] == preValue) {
count++;
} else {
count--;
}
}
}
int cnt = 0;
for (int i = 0; i < array.length; i++) {
if (array[i] == preValue) {
cnt++;
}
}
return cnt > array.length / 2 ? preValue : 0;
}
29、最小的K個數
題目描述
輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4,。
題目分析
解法一: 使用選擇排序,選出前k小的數字。
解法二: 利用快排的partition函數,找到某個最終位置爲k的pivlot。時間複雜度可達O(nlogk)。未實現。
解法三: 遍歷數組,使用大小爲k的堆(PriorityQueue),維護到目前爲止最小的k個數。時間複雜度可達O(nlogk)。未實現。
代碼實現
解法一: O(nk)
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
if (input == null || input.length == 0 || k > input.length) {
return new ArrayList<>();
}
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < k; i++) {
int minIndex = i;
for (int j = i + 1; j < input.length; j++) {
minIndex = input[j] < input[minIndex] ? j : minIndex;
}
list.add(input[minIndex]);
swap(input, i, minIndex);
}
return list;
}
public void swap(int[] arr, int i, int j) {
if (i == j) return;
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
}
30、連續子數組的最大和
題目描述
給一個數組,返回它的最大連續子序列的和。(子向量的長度至少是1)
例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和爲8(從第0個開始,到第3個爲止)
題目分析
解法一: 典型的動態規劃。dp[n]代表以當前元素爲截止點的連續子序列的最大和,如果dp[n-1]>0,dp[n]=dp[n]+dp[n-1],因爲當前數字加上一個正數一定會變大;如果dp[n-1]<0,dp[n]不變,因爲當前數字加上一個負數一定會變小。使用一個變量max記錄最大的dp值返回即可。
代碼實現
解法一: O(n)
public int FindGreatestSumOfSubArray(int[] array) {
int max = array[0];
for (int i = 1; i < array.length; i++) {
array[i] += array[i - 1] > 0 ? array[i - 1] : 0;
max = Math.max(max, array[i]);
}
return max;
}