2019 iOS面試題大全---全方面剖析面試
2018 iOS面試題---算法相關
1、七種常見的數組排序算法整理(C語言版本)
2、2019 算法面試相關(leetcode)--數組和鏈表
3、2019 算法面試相關(leetcode)--字符串
4、2019 算法面試相關(leetcode)--棧和隊列
5、2019 算法面試相關(leetcode)--優先隊列
6、2019 算法面試相關(leetcode)--哈希表
7、2019 算法面試相關(leetcode)--樹、二叉樹、二叉搜索樹
8、2019 算法面試相關(leetcode)--遞歸與分治
9、2019 算法面試相關(leetcode)--貪心算法
10、2019 算法面試相關(leetcode)--動態規劃(Dynamic Programming)
11、2019 算法面試相關(leetcode)--動態規劃之背包問題
翻轉二叉樹
二叉樹的前序遍歷
二叉樹的中序遍歷
二叉樹的后序遍歷
驗證二叉搜索樹
二叉樹的最近公共祖先
二叉搜索樹的最近公共祖先
樹是一種數據結構,它是由n(n>=1)個有限結點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
每個結點有零個或多個子結點;沒有父結點的結點稱為根結點;每一個非根結點有且只有一個父結點;除了根結點外,每個子結點可以分為多個不相交的子樹
二叉樹(Binary Tree)是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用于實現二叉查找樹和二叉堆。
一棵深度為k,且有2^k-1個節點的二叉樹,稱為滿二叉樹。這種樹的特點是每一層上的節點數都是最大節點數。而在一棵二叉樹中,除最后一層外,若其余層都是滿的,并且最后一層或者是滿的,或者是在右邊缺少連續若干節點,則此二叉樹為完全二叉樹。具有n個節點的完全二叉樹的深度為floor(log2n)+1。深度為k的完全二叉樹,至少有2k-1個葉子節點,至多有2k-1個節點。
二叉查找樹(Binary Search Tree),(又:二叉搜索樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小于它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大于它的根結點的值; 它的左、右子樹也分別為二叉排序樹。
從二叉樹的定義來看,算是比較適用遞歸算法的數據結構了
二叉樹
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
一、 翻轉二叉樹
翻轉一棵二叉樹。
示例:
輸入:
4
/ \
2 7
/ \ / \
1 3 6 9
輸出:
4
/ \
7 2
/ \ / \
9 6 3 1
備注:
這個問題是受到 Max Howell 的 原問題 啟發的 :
谷歌:我們90%的工程師使用您編寫的軟件(Homebrew),但是您卻無法在面試時在白板上寫出翻轉二叉樹這道題,這太糟糕了。
又是經典的翻轉。
直接用遞歸解法,分別遞歸地交換左右子樹
var invertTree = function(root) {
if(!root) return root
let right = root.right
root.right = invertTree(root.left)
root.left = invertTree(right)
return root
};
二、 二叉樹的前序遍歷
給定一個二叉樹,返回它的 前序 遍歷。
示例:
輸入: [1,null,2,3]
1
\
2
/
3
輸出: [1,2,3]
進階: 遞歸算法很簡單,你可以通過迭代算法完成嗎?
前序遍歷首先訪問根結點然后遍歷左子樹,最后遍歷右子樹。在遍歷左、右子樹時,仍然先訪問根結點,然后遍歷左子樹,最后遍歷右子樹。
簡單說其遍歷順序就是:根--左--右
使用遞歸可以很容易實現。
var preorderTraversal = function(root) {
return helper(root,[])
};
var helper = function(root,nums){
if(!root) return nums
nums.push(root.val)
helper(root.left,nums)
helper(root.right,nums)
return nums
}
不過題目有要求用迭代算法,這里我們可以想到之前提過的數據結構:棧
我們可以先將父結點的值放到數組里,然后將父節點入棧,等遍歷完左子樹,再出棧,去遍歷右子樹。
var preorderTraversal = function(root){
let treeStack = []
let res = []
while(root || treeStack.length){
while(root){
res.push(root.val)
treeStack.push(root)
root = root.left
}
if(treeStack.length){
root = treeStack.pop()
root = root.right
}
}
return res
}
三、 二叉樹的中序遍歷
給定一個二叉樹,返回它的中序 遍歷。
示例:
輸入: [1,null,2,3]
1
\
2
/
3
輸出: [1,3,2]
進階: 遞歸算法很簡單,你可以通過迭代算法完成嗎?
中序遍歷首先遍歷左子樹,然后訪問根結點,最后遍歷右子樹。若二叉樹為空則結束返回,否則:
(1)中序遍歷左子樹
(2)訪問根結點
(3)中序遍歷右子樹
簡單說其遍歷順序就是:左--根--右
同理,用遞歸也可以很簡單的實現。
var inorderTraversal = function(root) {
return helper(root,[])
};
var helper = function(root,nums){
if(!root) return nums
helper(root.left,nums)
nums.push(root.val)
helper(root.right,nums)
return nums
}
如果不用遞歸,用迭代呢?一樣,也可以用棧去實現,先把父結點入棧,去遍歷左子樹,然后出棧將父節點放進數組,在同樣去遍歷右子樹
var inorderTraversal = function(root){
let treeStack = []
let res = []
while(root || treeStack.length){
while(root){
treeStack.push(root)
root = root.left
}
if(treeStack.length){
root = treeStack.pop()
res.push(root.val)
root = root.right
}
}
return res
}
四、二叉樹的后序遍歷
給定一個二叉樹,返回它的 后序 遍歷。
示例:
輸入: [1,null,2,3]
1
\
2
/
3
輸出: [3,2,1]
進階: 遞歸算法很簡單,你可以通過迭代算法完成嗎?
后序遍歷首先遍歷左子樹,然后遍歷右子樹,最后訪問根結點,在遍歷左、右子樹時,仍然先遍歷左子樹,然后遍歷右子樹,最后遍歷根結點。即:
若二叉樹為空則結束返回,否則:
(1)后序遍歷左子樹
(2)后序遍歷右子樹
(3)訪問根結點
簡單說其遍歷順序就是:左--右--根
和前序中序一樣,用遞歸也可以很簡單的實現。
var postorderTraversal = function(root) {
return helper(root,[])
};
var helper = function(root,nums){
if(!root) return nums
helper(root.left,nums)
helper(root.right,nums)
nums.push(root.val)
return nums
}
那如果不用遞歸用迭代呢?
leetcode上的前序中序遍歷兩道題都是標注的中等難度,而這道題卻是標注成困難難度。
因為如果還是用之前的思路即還是用棧,是沒辦法直接實現的。
我們可以以根-右-左順序入棧,然后一直向左遍歷,直至左右子樹都為空。出棧并保存到數組中,然后一直重復以上,這里需要注意的是,由于父結點已經遍歷過了,這里一直出棧會造成死循環。所以我們還需要引入一個參數pre,來記錄上次遍歷的樹,如果是棧尾的左右子樹,則說明該結點已經遍歷過,不再重復遍歷。
var postorderTraversal = function(root) {
if(!root) return []
let treeStack = [root]
let res = []
let pre
while(treeStack.length){
root = treeStack[treeStack.length - 1]
if((!root.left && !root.right) || (pre &&(pre == root.left || pre == root.right))){
res.push(root.val)
treeStack.pop()
pre = root
}else{
if(root.right) treeStack.push(root.right)
if(root.left) treeStack.push(root.left)
}
}
return res
}
五、 驗證二叉搜索樹
給定一個二叉樹,判斷其是否是一個有效的二叉搜索樹。
假設一個二叉搜索樹具有如下特征:
節點的左子樹只包含小于當前節點的數。
節點的右子樹只包含大于當前節點的數。
所有左子樹和右子樹自身必須也是二叉搜索樹。
示例 1:
輸入:
2
/ \
1 3
輸出: true
示例 2:
輸入:
5
/ \
1 4
/ \
3 6
輸出: false
解釋: 輸入為: [5,1,4,null,null,3,6]。
根節點的值為 5 ,但是其右子節點值為 4 。
驗證二叉搜索樹,其實就是驗證其是否有序,我們可以對其進行中序遍歷,如果遍歷后的數據是有序的,則說明是二叉搜索樹,反之則不是。
var isValidBST = function(root) {
if(!root) return true
let nums = []
helper(root,nums)
for(let i = 0; i < nums.length - 1; i++){
if(nums[i] >= nums[i + 1]) return false
}
return true
}
var helper = function(root,nums){
if(!root) return
helper(root.left,nums)
nums.push(root.val)
helper(root.right,nums)
}
那么能不能用o(1)的空間復雜度來解決呢?事實上我們沒必要把數據都保存下來,對于右子樹而言,只要所有右子樹都大于其父節點即可,同理,所有左子樹小于其父節點即可。
我們可以用兩個變量:min和max,對于左子樹,則判斷是否小于min;對于右子樹,則判斷是否大于max
然后不斷遞歸遍歷,同時更新min和max
var isValidBST = function(root) {
return helper(root,undefined,undefined)
}
var helper = function(root,min,max){
if(!root ) return true
if(min != undefined && root.val <= min) return false
if(max != undefined && root.val >= max) return false
return helper(root.right,root.val,max) && helper(root.left,min,root.val)
}
六、 二叉樹的最近公共祖先
給定一個二叉樹, 找到該樹中兩個指定節點的最近公共祖先。
百度百科中最近公共祖先的定義為:“對于有根樹 T 的兩個結點 p、q,最近公共祖先表示為一個結點 x,滿足 x 是 p、q 的祖先且 x 的深度盡可能大(一個節點也可以是它自己的祖先)。”
例如,給定如下二叉樹: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
輸入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
輸出: 3
解釋: 節點 5
和節點 1
的最近公共祖先是節點 3。
示例 2:
輸入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
輸出: 5
解釋: 節點 5
和節點 4
的最近公共祖先是節點 5。
因為根據定義最近公共祖先節點可以為節點本身。
說明:
- 所有節點的值都是唯一的。
- p、q 為不同節點且均存在于給定的二叉樹中。
首先可以確定如果root為null或者p或q是根節點,那么說明最近的公共祖先就是根結點root。
我們同樣可以使用遞歸去實現,分別遞歸左右結點,將其設為根節點,如果返回null,說明公共祖先在另一側,如果都為null,說明公共結點就是root
var lowestCommonAncestor = function(root, p, q) {
if(root == null || root == p || root == q) return root
let left = lowestCommonAncestor(root.left,p,q)
let right = lowestCommonAncestor(root.right,p,q)
return !left ? right : (!right ? left : root)
}
或者我們可以同時分別求p和q的最近祖先
var findLowestAncestor = function(root,p){
if(root == null || root == p || root.left == p || root.right == p) return root
return findLowestAncestor(root.left,p) || findLowestAncestor(root.right,p)
}
一層一層往上遍歷,直到p和q的祖先相遇
var lowestCommonAncestor = function(root, p, q) {
let qSet = new Set()
qSet.add(q)
while(p != q){
if(qSet.has(p)) return p
while(q != root){
q = findAncestor(root,q)
if(p == q) return p
qSet.add(q)
}
p = findAncestor(root,p)
}
return p
};
七、 二叉搜索樹的最近公共祖先
給定一個二叉搜索樹, 找到該樹中兩個指定節點的最近公共祖先。
百度百科中最近公共祖先的定義為:“對于有根樹 T 的兩個結點 p、q,最近公共祖先表示為一個結點 x,滿足 x 是 p、q 的祖先且 x 的深度盡可能大(一個節點也可以是它自己的祖先)。”
例如,給定如下二叉搜索樹: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
輸入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
輸出: 6
解釋: 節點 2
和節點 8
的最近公共祖先是 6。
示例 2:
輸入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
輸出: 2
解釋: 節點 2
和節點 4
的最近公共祖先是 2
, 因為根據定義最近公共祖先節點可以為節點本身。
說明:
- 所有節點的值都是唯一的。
- p、q 為不同節點且均存在于給定的二叉搜索樹中。
題目中的p和q有可能兩個都在同側,在遞歸的過程中,另一側則沒有必要去遞歸了,但因為二叉樹無序,所以上邊的算法沒辦法去判斷,就算p和q不在另一側,也只能繼續遞歸下去。
而在二叉搜索樹中,上邊的那個算法就可以優化下
如果p和q都小于root的值,則說明p和q都在左側,向左邊遞歸即可;
如果p和q都大于root的值,則說明p和q都在右側,向右邊遞歸即可;
否則,就說明p和q在root的左右兩側,則其最近公共祖先就是root。
如果(root.val - p.val) 和 (root.val - q.val)的積是正的,則說明他們在同一側
var lowestCommonAncestor = function(root, p, q) {
return (root.val - p.val) * (root.val - q.val) <= 0 ? root : lowestCommonAncestor(root.val >= q.val ? root.left : root.right, p, q)
};
這里當然也可以不用遞歸
var lowestCommonAncestor = function(root, p, q) {
while ((root.val - p.val) * (root.val - q.val) > 0) root = p.val < root.val ? root.left : root.right
return root
};