数组与字符串 1.1 确定字符互异 描述:
给定一个字符串string str,请返回一个bool值,True 代表字符串的所有字符全都不同,False 代表存在相同的字符。保证字符串中的字符为ASCII字符且不允许使用额外的存储结构,字符串的长度小于等于3000。大小写字母算不同的字符
测试样例:
1 2 3 4 "aeiou" 返回:True "BarackObama" 返回:False
假设不使用额外的数据结构。
解法:
假定字符集为ASCII,若字符串的长度大于字母表的字符个数,直接返回false;字母表一共只有256个字符。 构建一个布尔值的数组,索引值i对应的标记该字符串是否含有字母表的第i个字符。若这个字符第二次出现,返回false。
1 2 3 4 5 6 7 8 9 10 bool checkDifferent (string str) { if (str.size () > 256 ) return false ; vector<bool > char_set (256 , 0 ) ; for (int i = 0 ; i < str.size (); i++) { int val = str[i]; if (char_set[val]) return false ; char_set[val] = true ; } return true ; }
1.2 原串翻转 描述:
给定一个string iniString ,请返回一个string,为该字符串翻转后的结果。要求不使用额外数据结构和储存空间,可以使用单个过程变量,保证字符串的长度小于等于5000。
测试样例:
1 2 "This is nowcoder" 返回:"redocwon si sihT"
解法:
不分配额外空间,直接就地翻转字符串。
1 2 3 4 5 6 7 8 9 10 11 12 string reverseString (string iniString) { if (iniString.size () < 2 ) return iniString; char tmp; int i = 0 , j = iniString.size () - 1 ; while (i < j) { tmp = iniString[i]; iniString[i++] = iniString[j]; iniString[j--] = tmp; } return iniString; }
1.3 确定两串乱序同构 描述:
给定string stringA 和string stringB ,编写程序确认两字符串包含的字符是否完全相同,注意大小写为不同字符,且考虑字符串中的空格,返回一个bool,代表两串是否由一样的字符组成。保证两串的长度都小于等于5000。
1 2 3 4 5 6 7 示例1 输入:, 返回值:true 示例2 输入:, 返回值:false
注:变位词区分大小写,空白也考虑在内。比较两个如果长度不同,就不可能是变位词。
解法1:排序字符串
1 2 3 4 5 6 7 bool checkSam (string stringA, string stringB) { if (stringA.size () != stringB.size ()) return false ; sort (stringA.begin (), stringA.end ()); sort (stringB.begin (), stringB.end ()); return stringA == stringB; }
解法2:检查两个字符串的各字符数是否相等
1 2 3 4 5 6 7 8 9 10 11 12 13 bool checkSam (string stringA, string stringB) { if (stringA.size () != stringB.size ()) return false ; vector<int > letters (256 , 0 ) ; for (char c : stringA) { letters[c]++; } for (char d : stringB) { letters[d]--; if (letters[d] < 0 ) return false ; } return true ; }
1.4 空格替换 描述:
给定一个string iniString 及其长度 int len , 已知该字符串中有空格,现要求编写程序将字符串中空格替换为“%20”。返回更改后的string。假设该字符串有足够的空间存放新增的字符,并且知道原字符的长度(小于等于1000),同时保证字符串由大小写的英文字母组成。
1 2 3 4 5 6 7 示例1 输入:"Mr John Smith" ,13 返回值:"Mr%20John%20Smith" 示例2 输入:"Hello World" ,12 返回值:"Hello%20%20World"
解法:
处理字符串操作问题,常用做法是从字符串尾部开始编辑,从后向前反向操作。因为字符串尾部有额外的缓存,可以直接修改,不必担心会覆写原来的数据。
两次扫描,一次先数出字符串中有多少空格,从而算出最终的字符串的长度;第二次扫描反向编辑字符串。检测到空格将%20复制到下一个位置,若不是空白,就复制原来的字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 string replaceSpace (string iniString, int length) { int count = 0 , newLength = 0 , i = 0 ; for (int i = 0 ; i < length; i++) { if (iniString[i] == ' ' ) count++; } newLength = length + 2 * count; string res (newLength, ' ' ) ; for (int i = length - 1 ; i >= 0 ; i--) { if (iniString[i] == ' ' ) { res[newLength - 1 ] = '0' ; res[newLength - 2 ] = '2' ; res[newLength - 3 ] = '%' ; newLength -= 3 ; } else { res[newLength - 1 ] = iniString[i]; newLength--; } } return res; }
1.5 基本字符串压缩 描述:
现给定一个string iniString字符串(长度小于等于10000),请按连续重复字母压缩的方式将该字符串压缩,返回结果为string,比如,字符串“aabbcccccaaa”经压缩会变成“a2b2c5a3”,若压缩后的字符串没有变短,则返回原先的字符串。注意保证串内字符均由大小写英文字母组成。
1 2 3 4 5 6 7 8 9 示例1 输入:"aabcccccaaa" 返回值:"a2b1c5a3" 示例2 输入:"welcometonowcoderrrrr" 返回值:"welcometonowcoderrrrr" 说明:welcometonowcoderrrrr转换成重复字母压缩的结果是w1e1 l1 c 1 o1 m1e1 t1 o1 n1 o1 w1 c 1 o1 d1e1 r5 ,比原字符串的长度还要长,所以返回原先的字符串。
解法:
加入压缩长度检查,算出压缩后的长度,构建相应大小的字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 string zipString (string iniString) { int size = countCompression (iniString); if (size >= iniString.size ()) return iniString; string res (size, ' ' ) ; int index = 1 , count = 1 ; char last = iniString[0 ]; res[0 ] = last; for (int i = 1 ; i < iniString.size (); i++) { if (iniString[i] == last) { count++; } else { last = iniString[i]; string tmp = to_string (count); for (char d : tmp) { res[index++] = d; } res[index++] = last; count = 1 ; } } string tmp = to_string (count); for (char d : tmp) { res[index++] = d; } return res; } int countCompression (string str) { if (str.size () == 0 ) return 0 ; char last = str[0 ]; int size = 0 , count = 1 ; for (int i = 1 ; i < str.size (); i++) { if (str[i] == last) { count++; } else { last = str[i]; size += 1 + to_string (count).size (); count = 1 ; } } size += 1 + to_string (count).size (); return size; }
1.6 像素翻转 描述:
现有一个NxN的矩阵,阶数为N,请编写一个算法将矩阵顺时针旋转90度并将其作为返回值。要求不使用缓存矩阵,保证N不大于500,元素不大于256,每个元素用int表示。
1 2 3 测试样例: [[1,2,3],[4,5,6],[7,8,9]] ,3 返回:[[7,4,1],[8,5,2],[9,6,3]]
解法:
按索引一个一个进行交换,从最外层开始逐渐向里,在每一层进行交换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 vector<vector<int > > transformImage (vector<vector<int > > mat, int n) { for (int layer = 0 ; layer < n / 2 ; layer++) { int first = layer, last = n - 1 - layer; for (int i = first; i < last; i++) { int offset = i - first; int top = mat[first][i]; mat[first][i] = mat[last - offset][first]; mat[last - offset][first] = mat[last][last - offset]; mat[last][last - offset] = mat[i][last]; mat[i][last] = top; } } return mat; }
1.7 清除行列 描述:给定一个N阶方阵int[][]
(C++中为vector<vector><int>>
)mat 及其阶数n ,若方阵中某个元素为0,则将其所在的行与列清零。返回改变后的int[][]
方阵(C++中为vector<vector><int>>
),保证n小于等于300,矩阵中的元素在nt范围内。
1 2 3 测试样例: [[1,2,3],[0,1,2],[0,0,1]] 返回:[[0,0,3],[0,0,0],[0,0,0]]
解法:
避免陷阱将矩阵所有元素清零。用两个数组记录包含零的所有行与列,第二次遍历矩阵时,若所在行或列标记为零,则将元素清零。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 vector<vector<int > > clearZero (vector<vector<int > > mat, int n) { vector<bool > row (n, 0 ) ; vector<bool > col (n, 0 ) ; for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { if (mat[i][j] == 0 ) { row[i] = true ; col[j] = true ; } } } for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { if (row[i] || col[j]) { mat[i][j] = 0 ; } } } return mat; }
1.8 翻转子串 描述:
给定2个字符串s1和s2,请判断s2是否为s1旋转而成,返回bool值。字符串中字符为英文字母和空格,区分大小写,字符串长度小于等于1000。
1 2 3 4 5 测试样例: "Hello world" ,"worldhello " 返回:false "waterbottle" ,"erbottlewat" 返回:true
解法:
假定s2由s1旋转而成,将s1划分为两部分:x和y,满足xy = s1和yx = s2.无论分割点在哪里,yx肯定是xyxy的子串,即s2为s1s1的子串。
1 2 3 4 5 6 7 8 9 bool checkReverseEqual (string s1, string s2) { int len = s1.size (); if (len == s2.size () && len > 0 ) { string s1s1 = s1 + s1; if (s1s1.find (s2) != -1 ) return true ; } return false ; }
链表 2.1 访问单个节点的删除 描述:
编写代码,移除未排序链表中的重复节点。 进阶:不使用临时缓冲区
解法:
使用散列表,直接迭代访问整个链表,将每个节点加入散列表,若发现重复元素,将该节点从链表中删除,然后继续迭代。一次扫描完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct ListNode { int val; struct ListNode *next ; ListNode (int x) : val (x), next (NULL ) {} }; void deleteDups (ListNode* n) { unordered_map<int , int > map; ListNode* pre = new ListNode (-1 ); while (n != nullptr ) { if (map.find (n->val) != map.end ()) { pre->next = n->next; } else { map[n->val] = 1 ; pre = n; } n = n->next; } }
进阶:不使用缓冲区。 用两个指针来迭代:current迭代访问整个链表,runner用于检查后续的节点是否重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void deleteDups (ListNode* head) { if (head == nullptr ) return ; ListNode* current = head; while (current != nullptr ) { ListNode* runner = current; while (runner->next != nullptr ) { if (runner->next->val == current->val) { runner->next = runner->next->next; } else { runner = runner->next; } } current = current->next; } }
2.2 倒数第k个节点 描述:
实现一个算法,找出单向链表中倒数第K个节点。
解法1:链表长度已知
若链表长度已知,那么倒数第k个节点就是第(length - k)个节点,直接迭代访问即可,比较简单。
解法2:递归
递归访问整个链表,当抵达链表末端时,该方法回传一个置为零的计数器,之后每次调用都会计数器加一。当计数器等于k时,表示访问的是倒数第k个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ListNode* nthToLast (ListNode* head, int k, int & i) { if (head == nullptr ) return nullptr ; ListNode* node = nthToLast (head->next, k, i); i++; if (i == k) { return head; } return node; } int main () { ListNode* l1 = new ListNode (10 ); l1 = fun (l1); int i = 0 ; ListNode* node = nthToLast (l1, 3 , i); }
2.3 删除某个节点 描述:
实现一个算法,删除单向链表中间的某个节点,假定只能访问该节点。
解法:
访问不到链表的首节点,只能访问待删除节点。解法很简单,直接将后继节点的数据复制到当前节点,然后删除该节点。
1 2 3 4 5 6 7 8 9 10 bool deleteNode (ListNode* n) { if (n == nullptr || n->next == nullptr ) { return false ; } ListNode* next = n->next; n->val = next->val; n->next = next->next; delete next; return true ; }
注意,若待删除节点为链表的尾节点,则这个问题无解。
2.4 链表分割 描述:
现有一链表的头指针 ListNode* pHead ,给一定值x,以x为基准将链表分割成两部分,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
解法:
不必移动和交换元素,直接创建两个链表,一个链表存储小于x的元素,一个链表存储大于等于x的元素。迭代访问整个链表,将元素插入before或after链表中。一旦抵达链表末端,表面拆分完成,最后合并两个链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ListNode* partition (ListNode* pHead, in ListNode* beforeStart = nullptr ; ListNode* beforeEnd = nullptr ; ListNode* afterStart = nullptr ; ListNode* afterEnd = nullptr ; ListNode* cur = pHead; while (cur != nullptr ) { ListNode* next = cur->next; cur->next = nullptr ; if (cur->val < x) { if (beforeStart == nullptr ) beforeStart = cur; beforeEnd = cur; } else { beforeEnd->next = cur; beforeEnd = beforeEnd-> } } else { if (afterStart == nullptr ) afterStart = cur; afterEnd = cur; } else { afterEnd->next = cur; afterEnd = afterEnd->ne } } cur = next; } if (beforeStart == nullptr ) return afterStart; beforeEnd->next = afterStart; return beforeStart; }
2.5 链式A+B 描述:
将两个反向存储在链表中的整数求和(即整数的个位存放在了链表首部,一位数对应一个节点),返回的结果仍用链表形式。给定两个链表ListNode* A ,ListNode* B ,请返回A+B的结果(ListNode*)。
1 2 3 4 5 测试样例: {1,2,3} , {3,2,1} 返回: {4,4,4} {7,1,6} , {5,9,2} 返回: {2,1,9}
解法:
递归模拟,两个节点的值相加,如有进位则转入下一节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ListNode* plusAB (ListNode* a, ListNode* b) { return addList (a, b, 0 ); } ListNode* addList (ListNode* a, ListNode* b, int carry) { if (a == nullptr && b == nullptr && carry == 0 ) return nullptr ; ListNode* res = new ListNode (0 ); int val = carry; if (a != nullptr ) { val += a->val; } if (b != nullptr ) { val += b->val; } res->val = val % 10 ; ListNode* more = addList (a == nullptr ? nullptr : a->next, b == nullptr ? nullptr : b->next, val >= 10 ? 1 : 0 ); res->next = more; return res; }
迭代遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ListNode* plusAB (ListNode* a, ListNode* b) { ListNode* haha = b; while (a != NULL ) { b->val += a->val; if (b->val >= 10 ) { b->val %= 10 ; if (b->next == NULL ) b->next = new ListNode (1 ); else b->next->val++; } if (b->next == NULL ){ b->next = a->next; break ; } a = a->next; b = b->next; } return haha; }
2.6 有环链表的环路开头节点 描述:
给定一个有环链表,实现一个算法返回环路的开头节点。
解法:
创建两个指针:fast和slow;
slow每走1步,fast走2步;
两者碰在一起时,将slow指向链表的头节点head,fast保持不变
以相同的速度移动slow和fast,一次走1步,返回新的碰撞处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ListNode* findBeginning (ListNode* head) { ListNode* slow = head; ListNode* fast = head; while (fast != nullptr && fast->next != nullptr ) { slow = slow->next; fast = fast->next->next; if (slow == fast) break ; } if (fast == nullptr || fast->next == nullptr ) { return nullptr ; } slow = head; while (slow != fast) { slow = slow->next; fast = fast->next; } return slow; }
2.7 回文链表 描述:编写一个函数,检查链表是否为回文。
1 2 3 4 5 测试样例: {1 ,2 ,3 ,2 ,1 } 返回:true {1 ,2 ,3 ,2 ,3 } 返回:false
解法1:反转并比较
反转整个链表,然后比较反转链表和原始链表。若两者相同,则该链表为回文。 注意,在比较原始链表和反转链表时,其实只需比较链表的前半部分。若原始链表和反转链表的前半部分相同,那么,两者的后半部分肯定相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 bool isPalindrome (ListNode* pHead) { ListNode* revered = reverseList (pHead); return isEqual (pHead, revered); } ListNode* reverseList (ListNode* head) { ListNode* res = nullptr ; ListNode* cur = head; while (cur != nullptr ) { ListNode* n = new ListNode (cur->val); n->next = res; res = n; cur = cur->next; } return res; } bool isEqual (ListNode* a, ListNode* b) { while (a != nullptr && b != nullptr ) { if (a->val != b->val) { return false ; } a = a->next; b = b->next; } return a == nullptr && b == nullptr ; }
解法2:迭代法
将链表前半部分反转,利用栈来实现。 入栈有两种方式,若链表长度已知:可以用for循环迭代访问前半部分节点,将每个节点入栈; 若链表长度未知,使用快慢指针,迭代访问链表,在快指针到达链表尾部时,慢指针刚好在链表中间位置。 至此,栈里就存放了链表前半部分的所有节点,不过顺序是相反的。接下来,只需迭代访问链表余下节点。每次迭代时,比较当前节点和栈顶元素,若完成迭代时比较结果完全相同,则该链表是回文序列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 bool isPalindrome (ListNode* pHead) { ListNode* slow = pHead; ListNode* fast = pHead; stack<int > st; while (fast != nullptr && fast->next != nullptr ) { st.push (slow->val); slow = slow->next; fast = fast->next->next; } if (fast != nullptr ) { slow = slow->next; } while (slow != nullptr && !st.empty ()) { int top = st.top (); st.pop (); if (top != slow->val) { return false ; } slow = slow->next; } return true ; }
解法3:递归法
1 2 3 4 5 6 7 8 9 10 11 12 13 ListNode* myLeft = nullptr ; bool isPalindrome (ListNode* pHead) { myLeft = pHead; return tranverse (pHead); } bool tranverse (ListNode* right) { if (right == nullptr ) return true ; bool res = tranverse (right->next); res = (res && right->val == myLeft->val); myLeft = myLeft->next; return res; }
栈与队列 3.1 三合一 描述:
如何只用一个数组来实现三个栈。
解法:
固定分割:将整个数组三等分,并将每个栈的增长限制在各自的空间里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class FixedMultiStack {private : int numberOfStacks = 3 ; int stackCapacity; vector<int > values; vector<int > sizes; public : FixedMultiStack (int stackSize) : stackCapacity (stackSize) { values.resize (stackSize * numberOfStacks); sizes.resize (numberOfStacks); } void push (int stackNum, int value) throws FullStackException { if (isFull (stackNum)) { throw new FullStackException (); } sizes[stackNum]++; values[indexOfTop (stackNum)] = value; } int pop (int stackNum) { if (isEmpty (stackNum)) { throw new EmptyStackException (); } int topIndex = indexOfTop (stackNum); int value = values[topIndex]; values[topIndex] = 0 ; sizes[stackNum]--; return value; } bool isEmpty (int stackNum) { return sizes[stackNum] == 0 ; } bool isFull (int stackNum) { return sizes[stackNum] == stackCapacity; } int IndexOfTop (int stackNum) { int offset = stackNum * stackCapacity; int size = sizes[stackNum]; return offset + size - 1 ; } };
3.2 栈的最小值 描述:
请设计一个栈,除了 pop 与 push 函数,还支持 min 函数,其可返回栈元素中的最小值。执行 push、pop 和 min 操作的时间复杂度必须为O(1)。
解法:
每个节点记录当前最小值。这么一来,要找到 min,直接查看栈顶元素就能得到最小值。缺点是栈很大时,每个元素都要记录min,会浪费大量空间。改进:用其他的栈来记录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class StackWithMin {private : stack<int > s1; satck<int > s2; public : void push (int val) { if (val <= min ()) { s2.push (val); } s1.push (val); } void pop () { int val = s1.top (); if (val == min ()) { s2.pop (); } s1.pop (); } int min () { if (s2.empty ()) { return INT_MAX; } else { return s2.top (); } } };
3.3 集合栈 描述:
请实现一种数据结构SetOfStacks,由多个大小为size的栈组成,当前一个栈填满时,则新建一个栈,且也可以与普通栈一样拥有相同的push和pop操作。 现给定一个操作序列int[][2]
ope(C++为vector<vector<int>>
),若执行push操作则第一个数为1,第二个数为应push的数字;若执行pop操作,则第一个数为2,第二个数为空。返回值为int[] ,即为变动后的SetOfStacks,顺序从下到上,初始SetOfStacks为空,并保证数据合法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 vector<vector<int > > setOfStacks (vector<vector<int > > ope, int size) { int len = ope.size (); vector<vector<int >> ans; for (int i = 0 ; i < len; i++) { if (ope[i][0 ] == 1 ) { if (ans.empty () || ans.back ().size () == size) { ans.push_back ({}); } ans.back ().push_back (ope[i][1 ]); } else { ans.back ().pop_back (); if (ans.back ().empty ()) { ans.pop_back (); } } } return ans; }
3.4 双栈排序 描述:
给定一个int[] numbers
(C++中为vector<int>
),其中第一个元素为栈顶,请编写程序将栈进行升序排列(即最大元素位于栈顶 ),返回排序后的栈。要求最多使用一个额外的栈存放临时数据,但不得将元素复制到别的数据结构中。并注意这是一个栈,意味着排序过程中只能访问到最后一个元素。
1 2 3 测试样例: [1,2,3,4,5] 返回:[5,4,3,2,1]
解法:
s1为原先的栈,s2为最终排好序的栈,若要对s1排序,可以从s1逐一弹出元素,然后按顺序插入s2中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 vector<int > twoStacksSort (vector<int > numbers) { stack<int > s1; for (auto num : numbers) { s1.push (num); } stack<int > tmp; while (!s1.empty ()) { int top = s1.top (); s1.pop (); while (!tmp.empty () && tmp.top () > top) { s1.push (tmp.top ()); tmp.pop (); } tmp.push (top); } vector<int > res; while (!tmp.empty ()) { int top = tmp.top (); s1.push (top); res.push_back (top); tmp.pop (); } return res; }
树与图 4.1 二叉树平衡检查 描述:
平衡的定义如下,已知对于树中的任意一个结点,若其两颗子树的高度差不超过1,则我们称该树平衡。现给定指向树根结点的指针TreeNode* root
,请编写函数返回一个bool,表示该二叉树是否平衡。
解法:
递归访问整棵树,计算每个节点的两个子树的高度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bool isBalance (TreeNode* root) { if (root == nullptr ) return true ; int diff = getHeight (root->left) - getHeight (root->right); if (abs (diff) > 1 ) { return false ; } else { return isBalance (root->left) && isBalance (root->right); } } int getHeight (TreeNode* root) { if (root == nullptr ) return 0 ; return max (getHeight (root -> left), getHeight (root->right)) + 1 ; }
改进:getHeight
函数不仅可以检查高度,还能检查这棵树是否平衡。从根节点递归向下检查每棵子树的高度,设计checkHeight
函数,若子树平衡返回子树的实际高度,若子树不平衡返回-1并中断执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool isBalance (TreeNode* root) { if (root == nullptr ) return true ; if (checkHeight (root) == -1 ) { return false ; } else { return true ; } } int checkHeight (TreeNode* root) { if (root == nullptr ) return 0 ; int leftHeight = checkHeight (root->left); if (leftHeight == -1 ) return -1 ; int rightHeight = checkHeight (root->right); if (rightHeight == -1 ) return -1 ; int diff = leftHeight - rightHeight; if (abs (diff) > 1 ) { return -1 ; } else { return max (leftHeight, rightHeight) + 1 ; } }
4.2 高度最小的BST 描述:
给定一个元素各不相同的有序序列int[] vals(升序排列),请编写算法创建一棵高度最小的二叉查找树,并返回二叉查找树的高度。
解法:
递归方法创建高度最小的BST。将数组中间位置的元素插入树中,数组左半部分插入左子树,数组右半部分插入右子树,递归处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int buildMinimalBST (vector<int > vals) { TreeNode* root = createMinBST (vals, 0 , vals.size () - 1 ); return getHeight (root); } TreeNode* createMinBST (vector<int > vals, int start, int end) { if (end < start) return nullptr ; int mid = start + (end - start) / 2 ; TreeNode* n = new TreeNode (vals[mid]); n->left = createMinBST (vals, start, mid - 1 ); n->right = createMinBST (vals, mid + 1 , end); return n; } int getHeight (TreeNode* root) { if (root == nullptr ) return 0 ; return max (getHeight (root -> left), getHeight (root->right)) + 1 ; }
4.3 输出单层节点 描述:
已知二叉树的根结点指针TreeNode* root以及链表上结点的深度,请设计算法返回一个链表ListNode,该链表代表该深度上所有结点的值,并按树上从左往右的顺序链接,深度不能超过树的高度,且树上结点的值为不大于100000的非负整数。
解法:
广度优先遍历,从根节点开始迭代,处于第i层时,表明访问过第i-1层的所有节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ListNode* getTreeLevel (TreeNode* root, int dep) { queue<TreeNode*> q; ListNode* node = new ListNode (-1 ); ListNode* res = node; q.push (root); int deep = 0 ; while (!q.empty ()) { int len = q.size (); for (int i = 0 ; i < len; i++) { TreeNode* cur = q.front (); q.pop (); if (deep == dep - 1 ) { ListNode* tmp = new ListNode (cur->val); node->next = tmp; node = node->next; } if (cur->left) q.push (cur->left); if (cur->right) q.push (cur->right); } deep++; } return res->next; }
4.4 检查是否为BST 描述:
现给定树的根结点指针TreeNode* root ,编辑函数返回一个bool值,判断该树是否为二叉查找树。
解法:
假定没有重复的值,可以采用中序遍历。BST中序遍历结果为递增序列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool checkBST (TreeNode* root) { vector<int > arr; process (arr, root); for (int i = 1 ; i < arr.size (); i++) { if (arr[i-1 ] > arr[i]) { return false ; } } return true ; } void process (vector<int >& arr, TreeNode* root) { if (root == nullptr ) return ; process (arr, root->left); arr.push_back (root->val); process (arr, root->right); }
解法二:最小与最大法
二叉搜索树的条件:所有左边的节点必须小于或等于当前节点,而当前节点必须小于所有右边的节点。 迭代遍历整个树,自上而下传递最小与最大值,逐渐变窄的范围检查各个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 bool checkBST (TreeNode* root) { return check (root, INT_MIN, INT_MAX); } bool check (TreeNode* root, int minval, int maxval) { if (root == nullptr ) return true ; if (root->val < minval || root->val > maxval) { return false ; } return check (root->left, minval, root->val) && check (root->right, root->val, maxval); }
4.5 寻找下一个节点 描述:
给定树的根结点指针TreeNode* root和结点的值int p,编写一个函数,寻找该二叉树中指定结点的下一个结点(即中序遍历的后继),并返回p结点的后继结点的值。保证结点的值是小于等于100000的正数且没有重复值,若不存在后继返回-1。
解法:
递归中序遍历。当节点值等于p,标记为true,返回遍历的下一个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int findSucc (TreeNode* root, int p) { bool sign = false ; return findSuccCore (root, p, sign); } int findSuccCore (TreeNode* root, int p, bool & sign) { if (root == nullptr ) return -1 ; int left = findSuccCore (root->left, p, sign); if (left != -1 ) return left; if (sign == true ) return root->val; if (root->val == p) sign = true ; return findSuccCore (root->right, p, sign); }
4.6 最近公共祖先 描述:
设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索树。
解法:
顺着一条 p 和 q 都在同一边的链子查找,也就是说,若 p 和 q 都在某节点的左边,就到左子树中查找共同祖先,若都在右边,则在右子树中查找共同祖先。要是 p 和 q不在同一边,那就表示已经找到第一个共同祖先。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 TreeNode* commonAncestor (TreeNode* root, TreeNode* p, TreeNode* q) { if (!cover (root, p) || !cover (root, q)) { return nullptr ; } return helper (root, p, q); } TreeNode* helper (TreeNode* root, TreeNode* p, TreeNode* q) { if (root == nullptr || root == p || root == q) { return root; } bool pIsOnLeft = cover (root->left, p); bool pIsOnRight = cover (root->right, q); if (pIsOnLeft != pIsOnRight) { return root; } TreeNode* childSize = pIsOnLeft ? root->left : root->right; return helper (childSize, p, q); } bool cover (TreeNode* root, TreeNode* p) { if (root == nullptr ) return false ; if (root == p) return true ; return cover (root->left, p) || cover (root->right, p); }
4.7 求和路径 描述:
给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。
解法1:暴力解法
遍历每个节点。对于每个节点,用递归法尝试所有向下的路径,并随着递归的进行跟踪路径的和。每当得到目标和,将发现的路径数目加一。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int countPathWithSum (TreeNode* root, int target) { if (root == 0 ) return 0 ; int pathFromRoot = countPathWithSumFromRoot (root, target, 0 ); int pathFromLeft = countPathWithSum (root->left, target); int pathFromRight = countPathWithSum (root->right, target); return pathFromRoot + pathFromLeft + pathFromRight; } int countPathWithSumFromRoot (TreeNode* node, int target, int sum) { if (node == nullptr ) return 0 ; sum += node->val; int totalPath = 0 ; if (sum == target) { totalPath++; } totalPath += countPathWithSumFromRoot (node->left, target, sum); totalPath += countPathWithSumFromRoot (node->right, target, sum); return totalPath; }
解法2:优化算法
使用哈希表减少重复计算。使用深度优先查找对树进行遍历。当我们访问每个节点时,执行以下操作。(1) 跟踪 runningSum 的值。我们将使该变量成为函数的一个参数,并对其增加 node.value。 (2) 在散列表中查找 runningSum - targetSum。我们从散列表获得的值为路径的总数。将变量 totalPaths 的值设置为该值。 (3) 如果 runningSum == targetSum,则发现了另外一条从根节点开始的路径。将变量 totalPaths加 1。 (4) 将 runningSum 加入到散列表中(如果 runningSum 已经存在,则将增加其值)。 (5) 对左子树和右子树进行递归,计算和为 targetSum 的路径的条数。 (6) 对左子树和右子树的递归调用结束后,减少散列表中 runningSum 对应的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int countPathWithSum (TreeNode* root, int target) { unordered_map<int , int > map; return process (root, target, 0 , map); } int process (TreeNode* node, int target, int sum, unordered_map<int , int >& map) { if (node == nullptr ) return 0 ; sum += node->val; int diff = sum - target; int totalPath = map[diff]; if (sum == target) { totalPath++; } map[sum]++; totalPath += countPathWithSum (node->left, target, sum, map); totalPath += countPathWithSum (node->right, target, sum, map); map[sum]--; return totalPath; }
递归与动态规划 5.1 加到n 描述:
给定一个正整数int n ,从0开始加到n,每次可增加1、2或3,直到其大于等于n,请返回一个数,代表加到n的方案的个数。保证n小于等于100000,并为了防止溢出,请将结果Mod 1000000007。
1 2 3 4 5 6 7 8 9 测试样例1: 1 返回:1 测试样例2: 3 返回:4 测试样例3: 4 返回:7
解法:
暴力解求解:递归,指数级增长,会超时。
1 2 3 4 5 6 7 8 9 int countWays (int n) { if (n < 0 ) { return 0 ; } else if (n == 0 ) { return 1 ; } else { return countWays (n - 1 ) + countWays (n - 2 ) + countWays (n - 3 ); } }
改进:制表法,利用memo数组记录中间过程的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int countWays (int n) { vector<int > memo (n + 1 , -1 ) ; return countWaysCore (n, memo); } int countWaysCore (int n, vector<int >& memo) { if (n < 0 ) { return 0 ; } else if (n == 0 ) { return 1 ; } else if (memo[n] > -1 ) { return memo[n]; } else { memo[n] = ((countWaysCore (n - 1 , memo) + countWaysCore (n - 2 , memo))% 1000000007 + countWaysCore (n - 3 , memo))% 1000000007 ; return memo[n]; } }
5.2 机器人走方格I 描述:
给定两个正整数int x ,int y ,代表一个x乘y的网格,现有一个机器人要从网格左上角顶点走到右下角,每次只能走一步且只能向右或向下走,返回机器人有多少种走法。保证x+y小于等于12。
解法:
假设网格r行c列,移动到(r, c), 需要先移动到相邻点(r-1, c)或(r, c-1)。 动态规划,用一个二维表dp记录每个点的走法,dp[r][c] = dp[r - 1][c] + dp[r][c - 1]
. 需要注意边界的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int countWays (int x, int y) { int dp[x + 1 ][y + 1 ]; for (int i = 1 ; i <= x; i++) { dp[i][1 ] = 1 ; } for (int j = 1 ; j <= y; j++) { dp[1 ][j] = 1 ; } for (int i = 2 ; i <= x; i++) { for (int j = 2 ; j <= y; j++) { dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; } } return dp[x][y]; }
5.3 机器人走方格II 描述:
给定一个int[][] map (C++ 中为vector >)网格图,若map[i][j]为1则该点不是障碍点,否则为障碍点。另外给定int x ,int y ,表示网格的大小。现有一个机器人要从网格左上角走到右下角,只能走格点且只能向右或向下走。请返回机器人从(0,0)走到(x - 1,y - 1)有多少种走法。请将结果Mod 1000000007以防止溢出,并保证x和y均小于等于50。
解法:
动态规划,建立dp表存储中间结果,如果map[r][c]
值不为1,则dp[r][c] = 0
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int countWays (vector<vector<int > > map, int x, int y) { vector<vector<int >> dp (x, vector<int >(y, 0 )); dp[0 ][0 ] = 1 ; for (int i = 0 ; i < x; i++) { if (map[i][0 ] != 1 ) { dp[i][0 ] = 0 ; break ; } dp[i][0 ] = 1 ; } for (int j = 0 ; j < y; j++) { if (map[0 ][j] != 1 ) { dp[0 ][j] = 0 ; break ; } dp[0 ][j] = 1 ; } for (int i = 1 ; i < x; i++) { for (int j = 1 ; j < y; j++) { if (map[i][j] == 1 ) { dp[i][j] = (dp[i-1 ][j] + dp[i][j-1 ]) % 1000000007 ; } else { dp[i][j] = 0 ; } } } return dp[x - 1 ][y - 1 ]; }
5.4 魔术索引 描述:
已知数组A [0..n-1]和数组大小n (升序数组,元素值各不相同),若存在A[i]=i则称该数组有魔术索引,请判断该数组是否存在魔术索引,返回值为bool,要求复杂度优于o(n)。
1 2 3 测试样例: [1,2,3,4,5] 返回:false
解法:
有序数组,二分查找,要找出元素k,会先拿它跟数组中间的元素 x比较,确定k位于x的左边还是右边。递归二分查找:(数组中没有重复的值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool findMagicIndex (vector<int > A, int n) { return process (A, 0 , A.size () - 1 ); } bool process (vector<int > arr, int start, int end) { if (start > end) return false ; int mid = start + (end - start) / 2 ; if (arr[mid] == mid) { return true ; } else if (arr[mid] > mid){ return process (arr, start, mid - 1 ); } else { return process (arr, mid + 1 , end); } }
进阶:数组中有重复的值,判断是否存在魔术索引。
解法:如果数组元素有重复值,前面的算法就会失效。如果 A[mid] < mid,我们无法断定魔术索引位于数组哪一边。它可能在数组右侧,也可能在左侧。 二分递归:(也适用于不存在重复的值,暴力解法)
1 2 3 4 5 6 7 8 9 10 11 12 13 bool findMagicIndex (vector<int > A, int n) { return process (A, 0 , A.size () - 1 ); } bool process (vector<int > arr, int start, int end) { if (start > end) return false ; int mid = start + (end - start) / 2 ; if (arr[mid] == mid) { return true ; } return process (arr, start, mid - 1 ) || process (arr, mid + 1 , end); }
5.5 子集 描述:
已知数组A 和其大小n ,请返回A的所有非空子集。要求A中元素个数不大于20且互异。各子集内部从大到小排序,子集间字典逆序排序。
1 2 3 测试样例: [123,456,789] 返回:{[789,456,123] ,[789,456] ,[789,123] ,[789] ,[456 123] ,[456] ,[123] }
解法:
递归,path记录中间过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 vector<vector<int >> res; vector<int > path; vector<vector<int > > getSubsets (vector<int > A, int n) { sort (A.begin (), A.end (),[](int a, int b) {return a > b;}); recur (A, 0 ); reverse (res.begin (), res.end ()); return res; } void recur (vector<int >& arr, int index) { if (!path.empty ()) res.push_back (path); if (index >= arr.size ()) return ; for (int i = arr.size () - 1 ; i >= index; i--) { path.push_back (arr[i]); recur (arr, i + 1 ); path.pop_back (); } }