剑指offer(I)

90 阅读43分钟

本文中题解及图片部分参考自leetcode题解区,著作权归原作者所有。此文章仅作个人学习记录之用。

题目9

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

解1

class CQueue {
    stack<int> A,B;
public:
    CQueue() {
        while(!A.empty()){
            A.pop();
        }
        while(!B.empty()){
            B.pop();
        }
    }
    
    void appendTail(int value) {
        A.push(value);
    }
    
    int deleteHead() {
        if(A.empty()){
            return -1;
        }
        while(!A.empty()){
            B.push(A.top());
            A.pop();
        }
        int del = B.top();
        B.pop();
        while(!B.empty()){
            A.push(B.top());
            B.pop();
        }
        return del;
    }
};

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue* obj = new CQueue();
 * obj->appendTail(value);
 * int param_2 = obj->deleteHead();
 */

思路很简单,两个栈A、B,要入队就压入栈A,要出队就将A中元素倒入B,然后B的栈顶弹出,B中剩余元素再倒回A。但是性能很差,猜测是频繁让A、B入栈出栈导致的

解2

class CQueue {
private:
   stack<int> A,B;
   int size;

public:
   CQueue() {
       while(!A.empty()){
           A.pop();
       }
       while(!B.empty()){
           B.pop();
       }
       size = 0;
   }
   
   void appendTail(int value) {
       A.push(value);
       size++;
   }
   
   int deleteHead() {
       if(size == 0){
           return -1;
       }
       if(B.empty()){
           while(!A.empty()){
               B.push(A.top());
               A.pop();
           }
       }
       int del = B.top();
       B.pop();
       size--;
       return del;
   }
};

/**
* Your CQueue object will be instantiated and called as such:
* CQueue* obj = new CQueue();
* obj->appendTail(value);
* int param_2 = obj->deleteHead();
*/

思路是减少入栈和出栈操作,A只管入队,B只管出队,由size来判断当前队列的情况。因为队列是先进先出,只有上一次从A到B的所有元素都弹出了才会弹后面进入A的,所以每次当B中元素都弹完了再倒A也可以,性能有所提升。

题目30

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

class MinStack {
   stack<int> s, t;
   int min_ = 0;
public:
   /** initialize your data structure here. */
   MinStack() {
       while(!s.empty()){
           s.pop();
       }
       while(!t.empty()){
           t.pop();
       }
       min_ = 0;
   }
   
   void push(int x) {
       if(s.empty()){
           min_ = x;
           t.push(x);
       }
       else{
           if(x <= min_){
               min_ = x;
               t.push(x);
           }
       }
       s.push(x);
   }
   
   void pop() {
       if(s.top() == t.top()){
           t.pop();
           if(!t.empty())
               min_ = t.top();
       }
       s.pop();
   }
   
   int top() {
       return s.top();
   }
   
   int min() {
       return min_;
   }
};

/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->min();
*/

由于要求O(1)的操作,所以需要一个辅助栈来实现min的O(1)操作,pop()、push()、top()就用一个普通的栈即可。每次有新元素push时,如果其小于等于当前的最小数,则将其放入辅助栈。

  • 易遗忘等于的情况,因为插入元素可能和当前最小值相同
  • 易忘记第一个元素应放入辅助栈,且min的初始值应为第一个元素
  • 在pop()中min_ = t.top();前忘记判断t非空

题目6

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

/**
* Definition for singly-linked list.
* struct ListNode {
*     int val;
*     ListNode *next;
*     ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
   vector<int> reversePrint(ListNode* head) {
       stack<int> st;
       ListNode* p = head;
       while(p != nullptr){
           st.push(p->val);
           p = p->next;
       }
       vector<int> ans;
       while(!st.empty()){
           ans.push_back(st.top());
           st.pop();
       }
       return ans;
   }
};

要求反向打印,就从头到尾遍历链表并入栈,然后出栈放入数组即可。

题目24

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

解1

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head == NULL) return NULL;
        ListNode* a = head;
        if(head->next != NULL){
            ListNode *b = head->next, *tmp = head;
            while(tmp != NULL){
                tmp = b->next;
                b-> next = a;
                a = b;
                b = tmp;
            }
        } 
        head->next = NULL;
        return a;
    }
};

这是迭代解法,在遍历链表的过程中完成翻转

  • 特殊情况(head为空)先判断
  • b初始是head->next,所以也要判断head->next != NULL才能给b赋值
  • 将某节点的next指向前一个节点后其原来的next就丢失了,所以要在翻转前用tmp存原来的next

解2

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head == NULL || head->next == NULL ) return head;
        ListNode* ans = reverseList(head->next);
        head->next->next = head;
        head->next = NULL;
        return ans;
    }
};

这是递归解法,当一个问题可以拆成若干个同类型的子问题时可以考虑递归解法,有两个要素:

  • 终止条件
  • 递推公式 本题中,定义F(n) = 翻转以节点n为头的链表,则有:

F(n) = F(n->next) + [n->next->next = n] + [n->next = NULL]

这里加号不表示数学上的加法运算,而是需要依次执行的几个步骤

终止条件则为链表只有头结点时,此时无需翻转,直接返回头结点,对应head->next == NULL这个判断条件。我遗漏了空链表的情况,空链表则直接返回NULL,于是判断条件为head == NULL

题目35

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

解1

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head == nullptr) return nullptr;
        Node* cur = head;
        unordered_map<Node*, Node*> map;
        while(cur != nullptr){
            Node* new_node = new Node(cur->val);
            map[cur] = new_node;
            cur = cur->next;
        }
        cur = head;
        while(cur != nullptr){
            map[cur]->next = map[cur->next];
            map[cur]->random = map[cur->random];
            cur = cur->next;
        }
        return map[head];
    }
};

顺序复制链表会出现的问题就是random指向的节点可能还未创建,所以遍历两遍链表,第一遍把所有节点都复制下来,第二遍把next和random复制下来。 哈希表是原节点到新对应节点的映射。

  • 想过不用哈希表,就用一个vector<Node*>存储新节点,但那实际上是相当于建立了一个unorder_map<int, Node*>。用int作为key的话,如果指针非空是可以的,cur到head的距离就是这里需要的int。但问题在于原节点的next和random有可能是nullptr,一旦出现nullptr就没法计算与head的距离了,而用哈希表key反正都是一个指针就包括了nullptr的情况,不需要再处理。

解2

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head == nullptr) return nullptr;
        Node* cur = head;
        while(cur != nullptr){ //构造原节点 1 -> 新节点 1 -> 原节点 2 -> 新节点 2 -> ……
            Node* new_node = new Node(cur->val);
            new_node->next = cur->next;
            cur->next = new_node;
            cur = new_node->next;
        }
        cur = head;
        while(cur != nullptr){
            if(cur->random != nullptr){
                cur->next->random = cur->random->next;//新节点的random
            }
            cur = cur->next->next;
        }
        Node* ans = head->next, *p = head->next;
        cur = head;
        while(cur != nullptr){//拆分新旧链表
            cur->next = cur->next->next;
            cur = cur->next;
            if(ans->next != nullptr){
                ans->next = ans->next->next;
                ans = ans->next; 
            }
        }
        return p;
    }
};

解法二要遍历三遍链表,省下了哈希表的空间。
第一次遍历:构造原节点 1 -> 新节点 1 -> 原节点 2 -> 新节点 2 -> ……的形式,即在原节点之间插入新节点
第二次遍历:完成新节点的random
第三次遍历:拆分新旧链表的节点,自然就完成了新节点的next

  • 思路也不复杂,但总忘nullptr的情况,特殊情况记得优先考虑

题目5

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

class Solution {
public:
    string replaceSpace(string s) {
        for(int i = 0; i < s.length(); i++){
            if(s[i] == ' '){
                s[i] = '%';
                s.insert(i+1,"20");
            }
        }
        return s;
    }
};

length()返回字符串长度,insert()在指定位置插入字符串

题目58

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        string s1 = s.substr(0,n);
        string s2 = s.substr(n,s.length() - n);
        s2.insert(s2.length(),s1);
        return s2;
    }
};

substr()返回从指定位置开始指定长度的子串

题目3

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

解1

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int n = nums.size();
        vector<int> v(n,0);
        for(int i = 0; i < n; i++){
            if(v[nums[i]] == 0){
                v[nums[i]] = 1;
            }
            else return nums[i];
        }
        return 0;
    }
};

已知数字是0~n-1范围内的,开一个大小n的数组记录哪些数字出现过即可。

解2

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        unordered_map<int, bool> m;
        for(auto i : nums){
            if(m[i]) return i;
            m[i] = true;
        }
        return 0;
    }
};

用哈希表更普适,即使没给数字范围也可以。记得应该先判断再置true

题目53(I)

统计一个数字在排序数组中出现的次数。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int count = 0;
        for(auto i : nums){
            if(i == target) count++;
            if(i > target) break;
        }
        return count;
    }
};

由于是排序数组,只要当前遍历到比目标大就可以停止了

题目53(II)

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

解1

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int sum = (n+1)*n/2;
        for(auto i : nums){
            sum -= i;
        }
        return sum;
    }
};

0~n-1的和减去数组中的数字就是未出现的,需要遍历一遍数组,O(n)

解2

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int i = 0, j = n - 1;
        while(i <= j){
            int mid = (i + j) / 2;
            if(nums[mid] > mid) j = mid - 1;
            else i = mid + 1;
        }
        return i;
    }
};

有序 即可考虑二分查找,O(logn)

在使用二分查找时应注意while中的条件是i < j 还是 i <= j
while(i <= j) 搜索的是闭区间 [i, j],也就是说闭区间内的每一个元素都会被搜索,循环退出时 i = j + 1。while(i < j) 搜索的是左闭右开区间 [i, j),也就是说区间内除了 j 指向的每一个元素都会被搜索,循环退出时 i = j。在循环中对i,j重新赋值时也应根据选取的循环条件的意义看到底是mid + 1还是mid - 1

题目10

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

不能递归,会爆栈的

解1

class Solution {
public:
    int fib(int n) {
       vector<int> array;
       array.push_back(0);
       array.push_back(1);
       for(int i = 2; i <= n; i++){
           array.push_back((array[i-1] % 1000000007 +array[i-2] % 1000000007) % 1000000007);
       }
       return array[n];
    }
};

算是动态规划,把每一轮的结果都存入数组,就不用一直递归可以直接取到。

  • (a + b) % p = (a % p + b % p) % p

空间也可以继续优化,F(n)只与其前面两项有关,其实只需要三个位置就够了。

解2

class Solution {
public:
    int fib(int n) {
       if(n < 2) return n;
       int p = 0, q = 1, r = 1;
       int x = n - 2;
       while(x--){
           p = q;
           q = r;
           r = q % 1000000007 + p % 1000000007;
       }
       return r%1000000007;
    }
};

这是优化过的,空间只需要三个int。漏了n<2的情况

题目4

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解1

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if(matrix.size() == 0) return false;
        int line = matrix.size(), column = matrix[0].size();
        if(line == 0 || column == 0) return false;
        int i = 0 , j = 0; //i为起始搜索行数,j为结束搜索行数
        bool flag = false;
        while(i < line && matrix[i][column - 1] < target){
            i++;
        }
        if(i == line) return false;
        j = i;
        while(j < line && matrix[j][0] <= target && !flag){
            int l = 0, r = column - 1;//搜索范围内每一行都二分
            while(l <= r){
                int mid = (l + r) / 2;
                if(matrix[j][mid] > target) 
                    r = mid - 1;
                else if(matrix[j][mid] < target) 
                    l = mid + 1;
                else{
                    flag = true;
                    break;
                }
            }
            j++;
        }
        return flag;
    }
};

思路并不难,要充分利用有序这个条件,为了减少查找范围首先看每一行的最后一个元素,若其小于target这一行就可以直接跳过。从起始行开始对每一行二分查找,若某一行第一个元素大于target了就可以停止了。

注意点:

  • 二维vector获取行数列数
int line = matrix.size(), column = matrix[0].size();
  • 对特殊情况的判断遗漏,反复错了好几次
if(line == 0 || column == 0) return false;

一开始想用这个判断空输入,问题在于对于输入为[],column根本就不存在,在执行这个判断之前,给column赋值时就已经错了,所以只能在第一行就执行

if(matrix.size() == 0) return false;
  • 在while的判断条件中先后顺序也很重要
while(i < line && matrix[i][column - 1] < target)

i < line必须放在第一个,因为循环里i++,最后一次循环时i= line - 1,循环体结束后i = line,i < line写在前面就先判断不满足循环跳出去了,原来我写的是

while(matrix[i][column - 1] < target && i < line)

如果这样写那就会先判断matrix[i][column - 1] < target,而i已经等于line了,直接数组越界报错

所以以后凡是循环内涉及到数组下标的,一定要在判断条件中先判断有无越界

  • 二分查找参考前面的题对于l,r的赋值和while条件的选择没有问题,但是在找到对应元素时只写了flag = true,忘记break了,就一直死循环

题目11

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。

解1

class Solution {
public:
    int minArray(vector<int>& numbers) {
        if(numbers.size() == 1) return numbers[0];//1个元素
        int i = 0;
        while( i < numbers.size() - 1 && numbers[i + 1] >= numbers[i]){
            i++;
        }
        if(i == numbers.size() - 1) return numbers[0]; //没反转、全一样
        return numbers[i + 1];//正常
    }
};

最简单遍历数组,找到第一个比前一个元素小的就是答案,注意考虑各种特殊情况,O(n)。
但是有序就尽量用二分,找O(logn)的方法

解2

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0, right = numbers.size() - 1;
        while(left <= right){
            int mid = (left + right) / 2;
            if(numbers[mid] > numbers[right])
                left = mid + 1;
            else if(numbers[mid] < numbers[right])
                right = mid;
            else
                right--;
        }
         return numbers[left];
    }
};

某一个旋转数组的示意图: image.png 由旋转数组的定义:最小值右边的数字一定<=最小值左边的数字
所以我们就知道若numbers[mid] > numbers[right],则最小值一定在mid与right之间
若numbers[mid] < numbers[right],则最小值一定在left与mid之间
主要是numbers[mid] = numbers[right]时应该怎么考虑,其实是无法判断最小值在mid左边还是右边的,但是由于numbers[mid] = numbers[right],如果我们将right减小1,numbers[right]的值也仍然在[left,right]内,不会丢失数字,这种情况是最难想的

题目50

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

解1

class Solution {
public:
    char firstUniqChar(string s) {
        unordered_map<char, int> m;
        for(char i : s){
            m[i]++;
        }
        for(char i : s){
            if(m[i] == 1) return i;
        }
        return ' ';
    }
};

就遍历两次,第一次用哈希表存次数,第二次找第一个次数为1的
哈希表的value不显式初始化就默认为0,所以才能直接m[i]++

解2

class Solution {
public:
    char firstUniqChar(string s) {
        unordered_map<char, int> m;
        for(int i = 0; i < s.length(); i++){
            if(m.count(s[i]))
                m[s[i]] = -1;
            else
                m[s[i]] = i;
        }
        int min = s.length();
        for(auto [_, pos] : m){
            if(pos != -1) 
                min = min > pos ? pos : min;
        }
        return min == s.length() ? ' ' : s[min];
    }
};

解1中是遍历两次字符串,哈希表是<字符,次数>
解2遍历一次字符串一次哈希表,<字符,下标>
第一次遍历若哈希表中没有出现过该字符则存入下标,若出现过则置为-1。 第二次遍历哈希表中不为-1且下标最小的即为所求。相比于解1的优势在于当字符串很长且复杂时遍历哈希表比字符串快一些。

题目32(I)

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> levelOrder(TreeNode* root) {
        vector<int> ans;
        if(root == nullptr) return ans;
        TreeNode* t = root;
        queue<TreeNode*> q;
        q.push(t);
        while(!q.empty()){
            TreeNode* tmp = q.front();
            ans.push_back(tmp->val);
            q.pop();
            if(tmp->left)
                q.push(tmp->left);
            if(tmp->right)
                q.push(tmp->right);
        }
        return ans;
    }
};

模板题,直接记住,二叉树的层序遍历:BFS
根入队->访问队首->左孩子入队->右孩子入队->队首出队

题目32(II)

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

解1

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    class node{
    public:
        TreeNode* p;
        int depth;
        node(TreeNode* p, int depth){
            this->p = p;
            this->depth = depth;
        }
    };
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        TreeNode *t = root;
        queue<node> q;
        q.push(node(t, 0));
        while(!q.empty()){
            node n = q.front();
            while(ans.size() <= n.depth){
                ans.push_back(vector<int>());
            } 
            ans[n.depth].push_back(n.p->val);
            q.pop();
            if(n.p->left)
                q.push(node(n.p->left, n.depth + 1));
            if(n.p->right)
                q.push(node(n.p->right, n.depth + 1));
        }
        return ans;
    }
};

与(I)相比多了一个要求,每一层都要分开输出。为了判断每个节点所在层数新定义了一个类node,除了保存原来的节点还记录该节点的层数。
其余的与(I)一样,在输出时根据每个节点的depth输出到不同的vector即可。

注意:
二维的vector在添加元素之前不能直接对某一个vector进行操作,必须要先ans.push_back(vector<int>())
要保证想操作的vector先存在才行

解2

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        TreeNode* t = root;
        queue<TreeNode*> q;
        q.push(t);
        int num = 0;
        while(!q.empty()){
            ans.push_back(vector<int>());
            int len = q.size();
            while(len--){
                TreeNode* tmp = q.front();
                q.pop();
                ans[num].push_back(tmp->val);
                if(tmp->left){
                    q.push(tmp->left);
                }
                if(tmp->right){
                    q.push(tmp->right);
                }
            }
            num++;
        }
        return ans;
    }
};

不需要node类也能区分每一层,实际上队列本身就能做到。因为入队就是按层入的,只要知道每层有几个就行了。
每次都获取队列的长度,弹出这么多个元素(即把上一层全部弹出),再把他们的孩子入队(下一层全部入队)。
用num记录层数加入vector即可。

题目32(III)

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        TreeNode* t = root;
        deque<TreeNode*> deq, tmp;
        deq.push_back(t);
        int num = 0;
        while(!deq.empty()){
            int len = deq.size();
            ans.push_back(vector<int>());
            while(len--){
                TreeNode* cur = deq.front();
                deq.pop_front();
                if(cur->left){
                    deq.push_back(cur->left);
                }
                if(cur->right){
                    deq.push_back(cur->right);
                }
                if(num % 2 != 0){
                    tmp.push_front(cur);
                }
                else{
                    tmp.push_back(cur);
                }
            }
            for(auto p : tmp){
                ans[num].push_back(p->val);
            }
            num++;
            tmp.clear();
        }  
        return ans;                                                
    }
};

相比于(II)又要求按层交替输出,按层输出和(II)的做法一样,每次循环记录队列的长度,弹出这层的所有节点。 但是(II)中把每层节点从队列弹出后直接放进ans了,想要颠倒顺序输出就增添了一个双端队列tmp,根据奇偶层决定将deq中弹出的节点放在tmp头还是尾以此控制输出顺序,然后再把tmp中的元素放进ans。(总之就是用tmp完成颠倒顺序的功能,tmp也不一定非得双端队列,vector也可以,反转一下就行)

  • 记得每次都要清空tmp 双端队列deque的一些操作:
  • push/pop_back/front() 在队尾/队首添加/弹出元素
  • clear()清空
  • size()返回当前元素个数
  • max_size()返回队列最大容纳元素数
  • erase(pos)删除指定位置的元素
  • front()/back()返回队首/队尾元素

题目26

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构,即A中有出现和B相同的结构和节点值。

解1

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isContained(TreeNode* A, TreeNode* B){
        if(!B) return true;
        else{
            if(!A) return false;
            if(A->val == B->val){
                return isContained(A->left, B->left) && isContained(A->right, B->right);
            }
            else return false;
        }
    }
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(!A || !B) return false;
        stack<TreeNode*> st;
        TreeNode* root = A, *tmp = nullptr;
        while(root || !st.empty()){ //中序遍历A,找到与B相同的节点
            while(root){
                st.push(root);
                root = root->left;
            }
            tmp = st.top();
            st.pop();
            if(tmp->val == B->val){
                return isContained(tmp,B);
            }
            if(tmp->right){
                root = tmp->right;
            }
        }
        return false;

    }
};

思路:
先遍历A,找到与B值相同的节点,再判断以该节点为根的子树是否包含B为根的子树
判断过程参考了判断两颗二叉树相同的方法,判断当前节点后递归判断左右子树。
不同之处在于只要求包含B为根的子树就行,所以只要比较到B的空节点都算true。

解2

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isContained(TreeNode* A, TreeNode* B){
        if(!B) return true;
        else{
            if(!A) return false;
            if(A->val == B->val){
                return isContained(A->left, B->left) && isContained(A->right, B->right);
            }
            else return false;
        }
    }
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(!A || !B) return false;
        return isContained(A,B) || isSubStructure(A->left, B) || isSubStructure(A->right, B); 
    }
};

解1在非递归遍历,实际上递归遍历就可以,没想到这样。
isSubStructure()是判断A为根的树是否包含B为根的树,那无非就是不包含,直接包含,A的左子树包含,A的右子树包含,所以直接return这三种情况的或,若都没有就是false,有一种就是true。
而这样递归的过程本质上就是先序遍历A,这比起解法1的非递归中序遍历更快,且代码更简洁。

题目27

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* mirrorTree(TreeNode* root) {
        if(!root) return root;
        if(!root->left && !root->right) return root;
        TreeNode* tmp = root->left;
        root->left = mirrorTree(root->right);
        root->right = mirrorTree(tmp); 
        return root;
    }
};

树相关的题递归是很常用的办法
mirrorTree()的功能是交换root的左右孩子,返回root本身。注意我们认为调用该函数时root的左右子树已经调整完毕了,只需交换左右孩子即可。

对于树,打牢遍历基础。要像有序数组对二分敏感一样,做到树对递归敏感。

题目28

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

错解

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSame(TreeNode* A, TreeNode* B){
        if(!A && !B) return true;
        if((A == nullptr && B != nullptr) || (A != nullptr && B == nullptr)) return false;
        if(A->val == B->val){
            return isSame(A->left, B->left) && isSame(A->right, B->right);
        }
        else return false;
    }
    TreeNode* mirrorTree(TreeNode* root){
        if(!root) return root;
        if(!root->left && !root->right) return root;
        TreeNode* tmp = root->left;
        root->left = mirrorTree(root->right);
        root->right = mirrorTree(tmp);
        return root;
    }
    bool isSymmetric(TreeNode* root) {
        TreeNode* mirror = mirrorTree(root);
        return isSame(root, mirror);
    }
};

想参考前两题的做法,将该树镜像翻转,然后判断是否与原来完全一致。 犯了一个低级错误,mirrorTree()传参是传的指针,这直接就把原来的树镜像了,原来的树就没了,所以判断的时候root和mirror指的是同一个内存空间,都是镜像后的树根,return恒为真。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSame(TreeNode* A, TreeNode* B){
        if(!A && !B) return true;
        if((A == nullptr && B != nullptr) || (A != nullptr && B == nullptr)) return false;
        if(A->val == B->val){
            return isSame(A->left, B->left) && isSame(A->right, B->right);
        }
        else return false;
    }
    TreeNode* mirrorTree(TreeNode* root){
        if(!root) return root;
        if(!root->left && !root->right) return root;
        TreeNode* tmp = root->left;
        root->left = mirrorTree(root->right);
        root->right = mirrorTree(tmp);
        return root;
    }
    bool isSymmetric(TreeNode* root) {
        if(!root) return true;
        if(!root->left && !root->right) return true;
        if(!root->left || !root->right) return false;
        return root->left->val == root->right->val && isSame(root->left, mirrorTree(root->right));
    }
};

应该考虑对称的二叉树有什么性质,首先根的左右孩子应该val相等,其次左子树镜像之后应该与右子树完全一样。根据这个判断就对了。

题目10

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

解1

class Solution {
    const int divisor = 1e9+7;
public:
    int numWays(int n) {
        vector<int> ans(n + 2);
        ans[0] = 1;
        ans[1] = 1;
        for(int i = 2; i <= n; i++){
            ans[i] = ans[i-1] % divisor + ans[i-2] % divisor;
        }
        return ans[n] % divisor;
    }
};

定义F(n)为跳上 n 级的台阶总共有多少种跳法,则有F(n) = F(n-1) + F(n-2)
递归会超时,就用数组存前面的结果(动态规划)

解2

class Solution {
    const int divisor = 1e9+7;
public:
    int numWays(int n) {
        if(n == 1 || n == 0) return 1;
        int p = 1, q = 1, r = 2, num = n - 2;
        while(num--){
            p = q;
            q = r;
            r = (p + q) % divisor;
        }
        return r;
    }
};

和斐波那契数列一样,用三个变量循环前进即可,节省空间
其实p和q本身就已经取过余了,不用再取,只要加起来取一次就行

image.png

题目63

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int min = 1e9, max = 0;
        for(auto i : prices){
            if(min > i) min = i;
            if(i - min > max) max = i - min;
        }
        return max > 0 ? max : 0;
    }
};

我一开始想的是遍历数组,每到一天就认为在这天买入,然后在它后面找到价格最高的一天卖出计算利润取最大值。问题在于从前往后遍历数组,走到某天的时候它后面的数据还没有遍历,不知道最大值是多少。如果每一轮都搜索它后面的所有元素复杂度太高。
解法从另一个角度去想,遍历数组,每到一天就认为在这天卖出,在它前面找到价格最低的一天认为在那天买入的,计算利润取最大值。因为走到某一天时它前面的元素都已经走过了,所以可以直接取到前面的最小值。

这个解法有点反常识,因为现实生活中不可能决定了某一天卖再穿越回前几天买入。但实际上是我被题目的这个背景套住了,本质上就是给个数组找对于所有i<jprices[j]-prices[i]的最大值。我先入为主地套上现实背景就相当于每次都在确定了i后找j,实际上和确定了j找i是完全一样的效果,换个思路就很简单。

TIPS:确定了i后找j,和确定了j后找i的搜索过程不是完全一样的,但本题只要求最大值。这两种搜索过程中都一定会经过这个最大值,所以最后的返回结果没有区别。

题目42

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n);
        dp[0] = nums[0];
        int ans = dp[0];
        for(int i = 1; i < n; i++){
            dp[i] = max(dp[i-1] + nums[i], nums[i]);
            ans = max(dp[i], ans);
        }
        return ans;
    }
};

动态规划五步走:

  1. 确定dp数组的大小及下标含义
  2. 找到递推公式
  3. 初始化dp数组
  4. 明确遍历顺序
  5. 举例推导dp数组

本题dp[i]表示以nums[i]结尾的最大子数组和,则dp[i] = max(dp[i-1] + nums[i], nums[i]),初始化dp[0] = nums[0],显然是从前往后遍历,就解决了。

  • 在选取dp数组时要想清楚dp[i]的含义,必须保证整个dp数组可以覆盖所有情况才能确保结果正确

题目47

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int line = grid.size(), column = grid[0].size();
        vector<vector<int>> dp; //dp比grid多一行一列方便处理
        for(int i = 0; i <= line; i++){
            dp.push_back(vector<int>(column+1, 0));
        }
        for(int i = 0; i < line; i++){
            for(int j = 0; j < column; j++){
                dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]) + grid[i][j];
            }
        }
        return dp[line][column];
    }
};

dp[i+1][j+1]表示走到grid[i][j]时的最大价值,dp设计的比grid多一行一列是方便处理grid的第一行第一列,不用特殊处理。grid[i][j]的上一步只能是grid[i-1][j]grid[i][j-1],所以取他们之中较大的加上 grid[i][j]

 

题目46

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:

输入: 12258
输出: 5
解释: 122585种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi""mzi"

class Solution {
public:
    int translateNum(int num) {
        string s = to_string(num);
        int n = s.length();
        vector<int> ans(n + 1);
        ans[0] = 1;
        ans[1] = 1;
        for(int i = 2; i <= n; i++){//ans[i]表示i位数的翻译方法
            if(s[i - 2] == '1' || (s[i - 2] == '2' && s[i - 1] <= '5')){
                ans[i] = ans[i-1] + ans[i-2];
            }
            else{
                ans[i] = ans[i-1];
            }
        }
        return ans[n];
    }
};

题意即给一个数字问有多少种拆分的可能,注意拆分成两位数时必须小于26,且前导0不合法。那么很明显是按照所给数字的位数进行区分的。ans[i]表示i位数的翻译方法,讨论i位数时取决于第i-1位和第i位拼成的两位数能否小于26,若不能则就是在i-1位数的后面加了一位,所以ans[i] = ans[i-1];若能拼成合法的两位数则分成拼与不拼两种情况,ans[i] = ans[i-1] + ans[i-2]

题目48

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.length(), max = 0;
        if(n <= 1) return n;
        vector<int> ans(n);
        unordered_map<char,int> subscript;
        ans[0] = 1;
        for(int i = 1; i < n; i++){
            if(subscript[s[i]] == 0 && s[i] != s[0]){
                ans[i] = ans[i-1] + 1;
            }
            else{
                if(subscript[s[i]] > i-1 - ans[i-1])
                    ans[i] = i - subscript[s[i]];
                else{
                    ans[i] = ans[i-1] + 1;
                }
            }
            subscript[s[i]] = i;
            max = max > ans[i] ? max : ans[i];
        }
        return max;
    }
};

ans[i]定义为以s[i]结尾的不包含重复字符的最长子字符串长度。递推公式为:

  • 若以s[i-1]结尾的不包含重复字符的最长子字符串中无s[i]ans[i] = ans[i-1] + 1

  • 若以s[i-1]结尾的不包含重复字符的最长子字符串中有s[i]ans[i] = ans[i-1]对应的字符串中截去最后一个s[i]及之前部分剩余的长度+1

难就难在怎么判断以s[i-1]结尾的不包含重复字符的最长子字符串中有无s[i],解1使用了一个哈希表用来存放当前遍历过的s中每一个字符最后出现的位置。
当某一个字符第一次出现时肯定满足ans[i] = ans[i-1] + 1,由于value默认是0,而subscript[s[0]]s[0]第二次出现之前肯定也是0,所以必须排除掉s[i] == s[0]的情况才能确保当前分支下的字符都是第一次出现。
而对于不是第一次出现的字符,就要看它上一次出现时是否被包含在s[i-1]对应的字符串中,ans[i-1]就是s[i-1]对应的字符串的长度,只需要看subscript[s[i]] 与 i-1 - ans[i-1]的大小即可判断

题目18

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        //空链表
        if(!head) return head;
        //删除头结点
        if(head->val == val){
            ListNode* new_head = head->next;
            head->next = nullptr;
            return new_head; 
        }
        ListNode* cur = head->next, *pre = head;
        while(cur->val != val){
            cur = cur->next;
            pre = pre->next;
        }
        pre->next = cur->next;
        cur->next = nullptr;
        return head;
    }
};

注意特殊情况别漏了就行,一个是空链表,一个是删除头结点

题目22

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* res = head;
        int count = 0;
        while(res){
            res = res->next;
            count++;
        }
        res = head;
        count -= k;
        while(count--){
            res = res->next;
        }
        return res;
    }
};

题目25

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

解1

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if(!l1) return l2;
        if(!l2) return l1;
        ListNode* new_linklist = l1->val <= l2->val ? l1 : l2;
        ListNode* new_l1 = new_linklist, *new_l2 = new_l1 == l1 ? l2 : l1, *tmp = nullptr; 
        while(new_l1 && new_l2){
            if(!new_l1->next){
                new_l1->next = new_l2;
                break;
            } 
            if(new_l2->val <= new_l1->next->val){
                tmp = new_l2;
                new_l2 = new_l2->next;
                tmp->next = new_l1->next;
                new_l1->next = tmp;
                new_l1 = tmp;
            }
            else{
                tmp = new_l1;
                new_l1 = new_l1->next;
            }
        }
        if(!new_l1){
            tmp->next = new_l2;
        }
        return new_linklist;
    }
};

不想使用额外空间,就在原链表基础上合并。 new_linklist是l1、l2中开头较小的一个,以它作为新链表头。new_l1new_l2分别是new_linklist和另外一个,作为前移的指针。
犯了两个错误:

  • 一开始没有写if(!new_l1->next)这个判断,带来的问题是当new_l1已经是最后一个节点时new_l1->next->val就报错了。实际上这个问题测试一下只有一个元素的链表就应该能发现。
  • new_l2插入链表后,直接把new_l1置为tmp->next了,这是一个很严重的错误。不能只比较各自原有的节点啊,新插进来的节点也要比较,不然就错位了。

题目52

输入两个链表,找出它们的第一个公共节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(!headA || !headB) return nullptr;
        ListNode* A = headA, *B = headB;
        while(A || B){
            if(!A){
                A = headB;
                continue;
            } 
            if(!B){
                B = headA;
                continue;
            } 
            if(A == B) return A;
            else{
                A = A->next;
                B = B->next;
            }
        }
        return nullptr;
    }
};

这个思路太妙了,根本想不到。
让两个指针从起点开始走过相同的距离若相交肯定会走到同一个节点,若不相交就都为nullptr。
都同时向前走一步,自己所在的这个链表走完了就去另一个链表的开头继续走,这样两个指针走过的距离肯定是一样的。

题目21

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。

解1

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int n = nums.size();
        int front = 0, back = n - 1, tmp;
        while(front < back){
            if(nums[front] % 2  == 0 && nums[back] % 2 == 1){
                tmp = nums[front];
                nums[front] = nums[back];
                nums[back] = tmp;
                front++;
                back--;
            }
            else{
                if(nums[front] % 2  == 0 && nums[back] % 2 == 0){
                    back--;
                }
                else if(nums[front] % 2  == 1 && nums[back] % 2 == 1){
                    front++;
                }
                else{
                    front++;
                    back--;
                }
            }
        }
        return nums;
    }
};

就从前后各走各的,分别对奇偶四种情况各自处理就行。但这样代码太长,不优雅,而且ifelse多了也会变慢。

解2

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int n = nums.size();
        int front = 0, back = n - 1;
        while(front < back){
            while(front < back && nums[front] % 2 == 1){
                front++;
            }
            while(front < back && nums[back] % 2 == 0){
                back--;
            }
            swap(nums[front], nums[back]);
        }
        return nums;
    }
};

解2就是标准的快排流程,找到左边第一个偶数和右边第一个奇数,然后交换。比解1判断的少,更快。

题目57

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int n = nums.size();
        int i = 0, j = n - 1;
        vector<int> ans;
        while(i < j){
            if(nums[i] + nums[j] > target){
                j--;
            }
            else if(nums[i] + nums[j] < target){
                i++;
            }
            else{
                ans.push_back(nums[i]);
                ans.push_back(nums[j]);
                break;
            }
        }
        return ans;
    }
};

题目58(I)

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。

class Solution {
public:
    string reverseWords(string s) {
        string ans = "", tmp = "";
        stack<string> st;
        int n = s.length();
        if(n == 0) return ans;//判断空串
        for(int i = 0; i < n; i++){
            if(s[i] != ' '){
                tmp += s[i];
            }
            else{
                if(tmp.size()){//处理空格开头和连续空格
                    st.push(tmp);
                    tmp.clear();
                }
            }
        }
        if(tmp.size()){//最后一个单词没入栈
            st.push(tmp);
        }
        while(!st.empty()){
            ans += st.top();
            st.pop();
            ans += " ";
        }
        if(ans.length())//去掉最后一个多余的空格,而且要先判断ans是否为空
            ans.erase(ans.length() - 1);
        return ans;
    }
};

不难,但是必须细致,特殊情况一个都不能漏

题目12

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

class Solution {
public:
    int subscript = 0;
    bool DFS(vector<vector<char>>& board, string word, int i, int j, vector<vector<bool>>& visit,
    int subscript){
        int line = board.size(), column = board[0].size();
        if(i < 0 || i >= line || j < 0 || j >= column || board[i][j] != word[subscript] || visit[i][j] == true){
            return false;
        }
        if(subscript == word.length() - 1) return true;
        visit[i][j] = true;
        bool res = DFS(board, word, i-1, j, visit, subscript+1) || DFS(board, word, i+1, j, visit, subscript+1)   
        || DFS(board, word, i, j-1, visit, subscript+1) || DFS(board, word, i, j+1, visit, subscript+1); 
        visit[i][j] = false;
        return res;
    }

    bool exist(vector<vector<char>>& board, string word) {
        int line = board.size(), column = board[0].size();
        vector<vector<bool>> visit(line, vector<bool>(column));
        bool res = false; 
        for(int i = 0; i < line; i++){
            for(int j = 0; j < column; j++){
                if(DFS(board, word, i, j, visit,subscript))
                return true; 
            }
        }
        return false;
    }
};

矩阵搜索典型DFS问题。DFS使用递归,检查当前board[i][j]是否符合要求,若符合则继续递归(搜索下一步),不符合则返回上一步。注意的是上一步退出之后,当前步应重新置为未访问才能继续搜索(回溯)。

题目13

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

解1

class Solution {
public:
    bool calculator(int i, int j, int k){
        int sum = 0, divider = 1;
        while(i / divider != 0){
            sum += (i / divider) % 10;
            divider *= 10;
        }
        divider = 1;
        while(j / divider != 0){
            sum += (j / divider) % 10;
            divider *= 10;
        }
        return sum <= k;
    }

    bool Reachable(vector<vector<bool>>& visit, int i, int j){
        if(i == 0 && j == 0) return true;
        int line = visit.size(), column = visit[0].size();
        bool res = false;
        if(i-1 >= 0) res = res || visit[i-1][j];
        if(i+1 < line) res = res || visit[i+1][j];
        if(j-1 >= 0) res = res || visit[i][j-1];
        if(j+1 < column) res = res || visit[i][j+1];
        return res;
    }

    int movingCount(int m, int n, int k) {
        vector<vector<bool>> visit(m, vector<bool>(n, false));
        int res = 0;
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(calculator(i, j, k) && !visit[i][j] && Reachable(visit, i, j)){
                    visit[i][j] = true;
                    res++;
                }
            }
        }
        return res;
    }
};

暴力解法,遍历该矩阵挨个判断每个格子是否满足:

  • 数位和 < k
  • 未被访问过
  • 可达 其中数位和的计算如calculator()所示,%10取得个位数,/10去掉了个位数,继续%10取得百位数,以此类推
    Reachable()用来判断该点是否可达,若某点的上下左右任意一个点可达则该点可达

解2

class Solution {
public:
    int res = 0;
    bool calculator(int i, int j, int k){
        int sum = 0, divider = 1;
        while(i / divider != 0){
            sum += (i / divider) % 10;
            divider *= 10;
        }
        divider = 1;
        while(j / divider != 0){
            sum += (j / divider) % 10;
            divider *= 10;
        }
        return sum <= k;
    }

    void DFS(vector<vector<bool>>& visit, int i, int j, int k){
        int line = visit.size(), column = visit[0].size();
        if(i < 0 || i >= line || j < 0 || j >= column || !calculator(i,j,k) || visit[i][j]){
            return;
        }
        if(calculator(i,j,k) && !visit[i][j]){
            res++;
            visit[i][j] = true;
            DFS(visit, i, j+1, k);
            DFS(visit, i+1, j, k);
        }

    }

    int movingCount(int m, int n, int k) {
        vector<vector<bool>> visit(m, vector<bool>(n, false));
        DFS(visit, 0, 0, k);
        return res;
    }
};

解2用DFS,从 (0,0) 出发,只向右或向下走就能到达所有可达点,所以若某点符合条件,只要递归它右、下的两个点就可以了。若某点不符合条件,则它右、下的两个点一定也不符合,直接在这里剪枝无需继续走了。用一个全局变量记录结果。DFS+剪枝比暴力遍历略好一点。

题目34

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> ans;
    vector<int> tmp;
    void DFS(TreeNode* root, int target, int sum){
        tmp.push_back(root->val);
        sum += root->val;
        if(root->left){
            DFS(root->left, target, sum);
        }
        if(root->right){
            DFS(root->right, target, sum);
        }
        if(!root->left && !root->right && sum == target){
            ans.push_back(tmp);
        } 
        tmp.pop_back();    
    }
    vector<vector<int>> pathSum(TreeNode* root, int target) {     
        if(!root) return ans; 
        DFS(root, target, 0);
        return ans;
    }
};

DFS该树,走到叶子了就判断是否符合条件,子节点都走完了自己就该弹出了。

题目36

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

解1

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* left;
    Node* right;

    Node() {}

    Node(int _val) {
        val = _val;
        left = NULL;
        right = NULL;
    }

    Node(int _val, Node* _left, Node* _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
public:
    Node* treeToDoublyList(Node* root) {
        if(!root) return nullptr;
        Node* t = root, *tmp;
        stack<Node*> st;
        vector<Node*> seq;
        while(!st.empty() || t){
            while(t){
                st.push(t);
                t = t->left;
            }
            tmp = st.top();
            seq.push_back(tmp);
            st.pop();
            if(tmp->right){
                t = tmp->right;
            }
        }
        for(int i = 0; i < seq.size(); i++){
            if(i != 0)
                seq[i]->left = seq[i-1];
            else
                seq[i]->left = seq[seq.size()-1];
            if(i != seq.size() - 1)
                seq[i]->right = seq[i+1]; 
            else
                seq[i]->right = seq[0];
        }
        return seq[0];
    }
};

二叉搜索树即左子树<根<右子树,要变成有序递增序列只要中序遍历即可,用一个vector保存中序遍历的结果,然后调整各节点的指针即可。

解2

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* left;
    Node* right;

    Node() {}

    Node(int _val) {
        val = _val;
        left = NULL;
        right = NULL;
    }

    Node(int _val, Node* _left, Node* _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
public:
    Node* pre = nullptr, *head = nullptr, *tail = nullptr;
    void DFS(Node* cur){
        if(!cur) return;
        DFS(cur->left);
        if(!pre) head = cur;
        else pre->right = cur;
        cur->left = pre;
        pre = cur;
        DFS(cur->right);
    }

    Node* treeToDoublyList(Node* root) {
        if(!root) return root;
        DFS(root);
        tail = root;
        while(tail->right){
            tail = tail->right;
        }
        tail->right = head;
        head->left = tail;
        return head;
    }
};

在遍历过程中就能完成调整,不需要vector保存记录。但是必须是递归遍历,解1是非递归遍历。
定义一个pre表示当前节点中序遍历的前一个节点,head记录新链表头
pre为空说明当前就是头结点,按左中右递归遍历,在“中”这一步改变指针,并把pre置为cur,这样“右”的时候pre仍然是前一个节点。

题目54

给定一棵二叉搜索树,请找出其中第 k 大的节点的值。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int count = 0, ans;
    void RevDFS(TreeNode* root, int k){
        if(root->right) RevDFS(root->right, k);
        count++;
        if(count == k){
            ans = root->val;
            return;
        }
        if(root->left) RevDFS(root->left, k); 
    }
    int kthLargest(TreeNode* root, int k) {
        RevDFS(root,k);
        return ans;
    }
};

如何利用二叉搜索树的条件呢?中序遍历是递增序列,第k大那就是中序遍历序列的倒数第k个元素。
我们可以将中序遍历略作修改,左中右-->右中左,则返回新的遍历序列的第k个元素即可。递归按照右中左遍历至第k个元素则返回。

题目45

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

class Solution {
public:
    bool cmp_str(string A, string B){
        string AB = A + B, BA = B + A;
        int i = 0;
        while(i < AB.length()){
            if(AB[i] > BA[i]) return true;
            else if(AB[i] < BA[i]) return false;
            else i++;
        }
        return true;
    }

    int Partition(vector<string>& nums_str, int start, int end){
        string tmp = nums_str[start];
        while(start < end){
            while(start < end && !cmp_str(tmp, nums_str[end])){
                end--;
            }
            swap(nums_str[start], nums_str[end]);
            while(start < end && cmp_str(tmp, nums_str[start])){
                start++;
            }
            swap(nums_str[start], nums_str[end]);
        }
        nums_str[start] = tmp;
        return start;
    }

    void quick_sort(vector<string>& nums_str, int start, int end){
        if(start < end){
            int pivot = Partition(nums_str, start, end);
            quick_sort(nums_str, start, pivot-1);
            quick_sort(nums_str, pivot+1, end);
        } 
    }

    string minNumber(vector<int>& nums) {
        int n = nums.size();
        vector<string> nums_str(n);
        for(int i = 0; i < n; i++){
            nums_str[i] =  to_string(nums[i]);
        }
        string ans = "";
        quick_sort(nums_str, 0, nums_str.size()-1);
        for(auto i : nums_str){
            ans += i;
        }
        return ans;
    }
};

定义一种在该题中比较字符串大小的方法,套用在快排上对给定数组排序,然后输出即可。

题目61

从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

class Solution {
public:
    bool isStraight(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int min = -1;
        nums.push_back(-1);
        for(int i = 0; i < 5; i++){
            if(nums[i] == nums[i+1] && nums[i] != 0) return false;
            if(min == -1 && nums[i] != 0) min = nums[i];
        }
        if(min == -1) return true;
        return nums[4] - min < 5;
    }
};

先排序,sort()函数可以对指定范围内的元素增序排列。
主要是判断是否为顺子,无重复数字(除0外)&& 五个数的最值差距<5(除0外)即可以是顺子。

题目40

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

这是经典的TOP k问题,主要方法有类快排的分治法和堆两种

解1

class Solution {
public:
    int Partition(vector<int>& arr, int start, int end, int k){
        int tmp = arr[start];
        while(start < end){
            while(start < end && arr[end] >= tmp){
                end--;
            }
            arr[start] = arr[end];
            while(start < end && arr[start] <= tmp){
                start++;
            }
            arr[end] = arr[start];
        }
        arr[start] = tmp;
        return start;
    }

    void Quick_Sort(vector<int>& arr, int start, int end, int k){
        if(start >= end) return;
        int pivot = Partition(arr, start, end, k);
        if(pivot == k-1) return;
        else if(pivot > k-1) Quick_Sort(arr, start, pivot-1, k);
        else Quick_Sort(arr, pivot+1, end, k);
    }

    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        Quick_Sort(arr, 0, arr.size()-1, k);
        vector<int> ans;
        for(int i = 0; i < k; i++){
            ans.push_back(arr[i]);
        }
        return ans;
    }
};

借鉴快排的思想,只要前k个已经排好了就可以返回了,即pivot == k-1
注意快排的Partition()中怎么选的基准,选左侧元素为基准就得从右侧开始走,反了就不对了。

解2

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        priority_queue<int> pq;
        vector<int> ans;
        if(k == 0) return ans;
        for(int i = 0; i < k; i++){
            pq.push(arr[i]);
        }
        for(int i = k; i < arr.size(); i++){
            if(arr[i] >= pq.top()) continue;
            else{
                pq.push(arr[i]);
                pq.pop();
            }
        }
        while(!pq.empty()){
            ans.push_back(pq.top());
            pq.pop();
        }
        return ans;
    }
};

本题中只要把数据都放入大顶堆,最后只留下k个就是最小的k个。

关于priority_queue

STL中的priority_queue是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,priority_queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。底层容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty() size() front() push_back() pop_back()

  • 默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
  • 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。默认情况下priority_queue是大堆。 优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。

priority_queue的代码原型为:

template <typename T, typename Container=std::vector<T>, typename Compare=std::less<T>> class priority_queue

可以看到,priority_queue模板有3个参数:

  • 第一个参数是存储对象的类型
  • 第二个参数是存储元素的底层容器(可缺省,默认使用vector)
  • 第三个参数是函数对象,它定义了一个用来决定元素顺序的断言(可缺省,默认大顶堆)模板参数的中的函数对象可以不写()

其中第二个参数存储元素的底层容器可以是满足priority_queue所需要操作的任何底层容器,一般来说,可选vector和deque

其中第三个参数决定元素优先级的函数对象可以是标准库中定义好的一些比较仿函数,也可以是自定义的其他仿函数。其中默认是标准库中提供的less<T>函数——对应获得一个大顶堆,如果想获得一个小顶堆,可以改用greater<T>函数

//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int,vector<int>,less<int> >q;

//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)

如果要用自定义数据类型,就要自己实现该数据类型的比较方法,有两种做法:

#include <iostream>
#include <queue>
using namespace std;

//方法1
struct tmp1 //运算符重载<
{
    int x;
    tmp1(int a) {x = a;}
    bool operator<(const tmp1& a) const
    {
        return x < a.x; //大顶堆
    }
};

//方法2
struct tmp2 //重写仿函数
{
    bool operator() (tmp1 a, tmp1 b)
    {
        return a.x < b.x; //大顶堆
    }
};

int main()
{
    tmp1 a(1);
    tmp1 b(2);
    tmp1 c(3);
    priority_queue<tmp1> d;
    d.push(b);
    d.push(c);
    d.push(a);
    while (!d.empty())
    {
        cout << d.top().x << '\n';
        d.pop();
    }
    cout << endl;

    priority_queue<tmp1, vector<tmp1>, tmp2> f;
    f.push(b);
    f.push(c);
    f.push(a);
    while (!f.empty())
    {
        cout << f.top().x << '\n';
        f.pop();
    }
}

注意运算符重载的话一定要重载<号,系统是默认使用<号进行比较的,重载>号无效。
仿函数就是重载()运算符

题目41

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。

class MedianFinder {
public:
    priority_queue<int, vector<int>, greater<int> > min_heap;
    priority_queue<int, vector<int>, less<int> > max_heap;
    /** initialize your data structure here. */
    MedianFinder() {

    }
    
    void addNum(int num) {
        if(min_heap.size() == max_heap.size()){
            max_heap.push(num);
            min_heap.push(max_heap.top());
            max_heap.pop();
        }
        else{
            min_heap.push(num);
            max_heap.push(min_heap.top());
            min_heap.pop();
        }
    }
    
    double findMedian() {
        if(min_heap.size() == max_heap.size()){
            return (static_cast<double>(min_heap.top()) + max_heap.top()) / 2;
        }
        else{
            return min_heap.top();
        }
    }
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

建立一个 小顶堆min_heap和 大顶堆max_heap,各保存列表的一半元素,且规定:

  • min_heap保存数据流中较小的一半,长度为N2\frac{N}{2}(N为偶数),N+12\frac{N+1}{2}(N为奇数)
  • max_heap保存数据流中较大的一半,长度为N2\frac{N}{2}(N为偶数),N12\frac{N-1}{2}(N为奇数) 随后,中位数可仅根据 min_heap, max_heap 的堆顶元素计算得到。

设元素总数为 N = m + n ,其中 m 和 n 分别为 min_heap 和 max_heap 中的元素个数。 addNum(num) 函数:

  • m = n(即 N 为偶数):插入后N为奇数,所以需向 min_heap 添加一个元素。
    实现方法:将新元素 num 插入至 max_heap ,再将 max_heap 堆顶元素插入至 min_heap
  • m != n (即 N 为 奇数):插入后N为偶数,所以需向 max_heap 添加一个元素。 实现方法:将新元素 num 插入至 min_heap ,再将 min_heap 堆顶元素插入至max_heap

为什么要这样插入?
假设插入数字 num 遇到情况 1 。由于 num 可能属于 “较大的一半” (即属于 max_heap),因此不能将 num 直接插入至 min_heap 。而应先将 num 插入至 max_heap ,再将 max_heap 堆顶元素插入至 min_heap 。这样就可以始终保持 max_heap 保存较大一半、 min_heap 保存较小一半。

findMedian() 函数:

  • m = n( N 为 偶数):则中位数为 (min_heap 的堆顶元素 + max_heap 的堆顶元素) / 2。
  • m != n( N 为 奇数):则中位数为 min_heap 的堆顶元素。

题目55(I)

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

解1

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int max = -1, depth = 0;
    void DFS(TreeNode* root){
        depth++;
        if(!root->left && !root->right){
            max = depth > max ? depth : max;
        }
        if(root->left) DFS(root->left);
        if(root->right) DFS(root->right);
        depth--;
    }
    int maxDepth(TreeNode* root) {
        if(!root) return 0;
        DFS(root);
        return max;
    }
};

递归对树DFS,过程中记录深度,取深度的最大值返回即可。

解2

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(!root) return 0;
        return max(maxDepth(root->left), maxDepth(root->right)) + 1;
    }
};

解1的递归也复杂了,两行就能搞定。
定义maxDepth()为以root为根的子树的深度,显然树深 = max(左子树深度,右子树深度) + 1

题目55(II)

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int DFS(TreeNode* root, int depth, bool& isbalance){
        if(!root) return depth;
        int left = DFS(root->left, depth+1, isbalance);
        int right = DFS(root->right, depth+1, isbalance);
        if(left - right > 1 || left - right < -1)
            isbalance = false;
        return max(left, right);
    }
    bool isBalanced(TreeNode* root) {
        bool isbalance = true;
        DFS(root, 0, isbalance);
        return isbalance;
    }
};
  • 注意:左右子树均是平衡二叉树 ≠> 该树是平衡二叉树, 还需要左右子树高度差小于1
    DFS返回当前子树最深的节点的深度,传一个引用bool变量用来标记是否存在某子树不平衡,检验两子树的高度差是否符合,不符合就直接false。

题目64

求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

class Solution {
public:
    int sumNums(int n) {
        n && (n += sumNums(n-1));
        return n;
    }
};

用逻辑运算符的性质取代递归中的if判断

逻辑运算符的短路性质:
以逻辑运算符 && 为例,对于 A && B 这个表达式,如果 A 表达式返回 False\textit{False} ,那么 A && B 已经确定为 False\textit{False} ,此时不会去执行表达式 B。同理,对于逻辑运算符 ||, 对于 A || B 这个表达式,如果 A 表达式返回 True\textit{True} ,那么 A || B 已经确定为 True\textit{True} ,此时不会去执行表达式 B。

  • 注意n += sumNums(n-1)这个表达式外面必须加括号,否则它不是一个逻辑判断,只是一个运算

题目68(I)

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        TreeNode* cur = root;
        while(1){
            if(cur->val > p->val && cur->val > q->val){
                cur = cur->left;
            }
            else if(cur->val < p->val && cur->val < q->val){
                cur = cur->right;
            }
            else{
                break;
            }
        }
        return cur;
    }
};

利用好二叉搜索树,从根节点开始:

  • 若p、q均小于当前节点则说明应该向左走
  • 若p、q均大于当前节点则说明应该向右走
  • 其余情况(当前节点夹在p、q之间  || 等于p、q其中之一)则说明当前就是分岔口,即答案

题目68(II)

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

解1

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* ans;
    bool DFS(TreeNode* root, TreeNode* p, TreeNode* q){ //表示以root为根的子树中是否包含pq
        if(!root) return false;
        bool left = DFS(root->left, p, q), right = DFS(root->right, p, q);
        if((left && right) || ((root == p || root == q) && (left || right))){
            ans = root;
        }
        return left || right || (root == p || root == q);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        DFS(root, p, q);
        return ans;
    }
};

从二叉搜索树退化成二叉树了,那就只能递归遍历所有节点寻找ans
主要是这个递归函数的定义想不出来,表示以root为根的子树中是否包含p或q。
且判断ans的时候判断条件易遗漏(root == p || root == q) && (left || right),即当前节点就是pq之一的情况,返回值也是一样易遗漏(root == p || root == q)的情况。

解2

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == p || root == q || !root) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q), *right = lowestCommonAncestor(root->right, p, q);
        if(left && right){
            return root;
        }
        if(!left) return right;
        if(!right) return left;
        return nullptr;
    }
};

另一个角度的递归,这道题递归方法的核心:

我们很自然的将函数

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)

的返回值定义为节点p和q的公共祖先,但是我们可以多给它一层含义:

1. 若树里面存在p,也存在q,则返回他们的公共祖先。
2. 若树里面只存在p,或只存在q,则返回存在的那一个。
3. 若树里面即不存在p,也不存在q,则返回null。

这多余的第二层和第三层不太容易想到。

递归三步走:

  • 确定子问题是什么、递归函数的参数和返回值
  • 找到退出递归的条件
  • 考虑在一次递归中要做什么事情

题目7

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

解1

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { //建立以preorder[0]为根的子树
    	if(preorder.size() == 0 || inorder.size() == 0) return nullptr; //特殊情况
        TreeNode* res = new TreeNode(preorder[0]);
        if(inorder.size() == 1) return res; //叶子就返回

        int i = 0;
        for(i = 0; i < inorder.size(); i++){ //找到preorder[0]在inorder中的位置,左右分开
            if(inorder[i] == preorder[0])
                break;
        }
        
        vector<int> lin(inorder.begin(), inorder.begin() + i);
        vector<int> rin(inorder.begin() + i + 1, inorder.end());
        vector<int> lpre(preorder.begin() + 1, preorder.begin() + lin.size() + 1);
        vector<int> rpre(preorder.begin() + preorder.size() - rin.size(), preorder.end());

        TreeNode* left = buildTree(lpre, lin);
        TreeNode* right = buildTree(rpre, rin);
        res->left = left;
        res->right = right;
        return res;
    }

根据前序和中序遍历的特点,前序遍历的第一个结点一定是当前子树的根,在对应的中序遍历序列中找到它,以它为分界点将中序序列分成左右两半就分别对应左右子树的中序序列,但是子树的前序序列不好提取。
同一棵树虽然遍历方式可能不同,但节点个数不会变,根据左右子树中序序列的长度就可以在前序序列中截取对应长度的前序序列。则递归的参数可以获取到。
根据这个想法,递归结束条件应为传入的数组只有一个数字时,这说明当前节点是叶子,直接返回即可
但是这个递归条件漏了特殊情况,如果初始传入的树就只有左/右一棵子树,那在截取vector时是会截到空vector的,所以还得判断空vector直接返回nullptr才对。
我的递归思路没有错,但是由于所给函数的参数和返回值都限定了,这就导致函数体内部的处理不得不更复杂,像是空vector这种情况根本想不到,重写一个递归函数,由所给函数调用可以简单一点。

解2

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    unordered_map<int, int> m; //inorder中preorder元素的位置

    TreeNode* rec(vector<int>& preorder, vector<int>& inorder, int preleft, int preright, int inleft, int inright){
        if(preleft > preright) return nullptr;
        TreeNode* res = new TreeNode(preorder[preleft]);

        TreeNode* left = rec(preorder, inorder, preleft + 1, preleft + 1 + m[preorder[preleft]] - inleft - 1, inleft, m[preorder[preleft]] - 1);
        TreeNode* right = rec(preorder, inorder, preright - inright + m[preorder[preleft]] + 1, preright, m[preorder[preleft]] + 1, inright);
        res->left = left;
        res->right = right;
        return res;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = inorder.size() - 1;
        for(int i = 0; i <= n; i++){
            m[inorder[i]] = i;
        }
        return rec(preorder, inorder, 0, n, 0, n);
    }
};

解2的递归是自定义的函数,多传了四个参数进去代表下一次递归的数组范围,和解法1切割vector是等效的,但是改变一个int可比切割vector省事多了。
另一个优化是在主函数里直接用哈希表存下inorder中preorder元素的位置,这样每次递归过程中就只需要查哈希表而无需在inorder中搜索,也省了很多时间。

题目16

实现 pow(xn) ,即计算 x 的 n 次幂函数(即,xnx^n)。不得使用库函数,同时不需要考虑大数问题。

解1

class Solution {
public:
    double myPow(double x, int n) {
        if(x == 0) return 0;
        double ans = 1.0;
        long tmp = n;
        if(tmp < 0){
            x = 1 / x;
            tmp = -tmp;
        } 
        while(tmp){
            if(tmp & 1){
                ans *= x;
            }
            x *= x;
            tmp >>= 1;
        }
        return ans;
    }
};

快速幂模板,要记住。

对于任何十进制正整数 n,设其二进制为 bmb3b2b1b_m\,···\,b_3\,b_2\,b_1,(bib_i 为二进制某位值,i \in [1,m]),则有:

二进制转十进制: n = 1b1+2b2+4b3++2m1bm1b_1 + 2b_2 + 4b_3 + ··· + 2^{m-1}b_m

幂的二进制展开: xn=x1b1+2b2+4b3++2m1bm=x1b1x2b2x4b3x2m1bmx^n = x^{1\,b_1 \,+\, 2\,b_2 \,+\, 4\,b_3\, +\, ··· \,+\, 2^{m-1}b_m} = x^{1\,b_1}x^{2\,b_2}x^{4\,b_3}···x^{2^{m-1}\,b_m}

根据以上推导,可把计算 xnx^n 转化为解决以下两个问题:

  1. 计算 x1,x2,x4,,x2m1x^1, x^2, x^4, ···, x^{2^{m-1}}的值: 循环赋值操作 x=x2x = x^2即可;

  2. 获取二进制各位 b1,b2,b3,,bmb_1, b_2, b_3, ···, b_m的值: 循环执行以下操作即可。 n & 1(与操作): 判断 n 二进制最右一位是否为 1 ;
    n >> 1 (移位操作): n 右移一位(可理解为删除二进制最后一位)。

因此,应用以上操作,可在循环中依次计算 x20b1,x21b2,...,x2m1bmx^{2^{0}\,b_1}, x^{2^{1}\,b_2}, ..., x^{2^{m-1}\,b_m}的值,并将所有 x2i1bix^{2^{i-1}\,b_i}累计相乘即可。

bi=0b_i = 0 时: x2i1bi=1x^{2^{i-1}\,b_i} = 1
bi=1b_i = 1 时: x2i1bi=x2i1x^{2^{i-1}\,b_i} = x^{2^{i-1}}

若是负指数则将x置为 1x\frac{1}{x} ,并把指数变正,快速幂只能计算正指数。

  • 还要注意上面代码里的tmp,tmp的作用是在遇到负指数变正指数的过程中防止溢出
    由于n定义为int,范围是 2312311-2^{31}\sim2^{31} - 1,若n恰好为231-2^{31},执行n = -n就会超出int范围,
    所以必须用一个long类型的tmp接收n,再对tmp取绝对值

题目33

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

解1

class Solution {
public:
    bool rec(vector<int>& postorder, int left, int right){ //postorder[right]为根的子树
        if(left >= right) return true;
        int i;
        for(i = right; i >= left; i--){ //划分左右子树
            if(postorder[i] < postorder[right])
                break;
        }
        //验证左子树都小于根,划分过程就自动满足右子树大于根了
        for(int j = left; j <= i; j++){
            if(postorder[j] > postorder[right])
                return false;
        }
        return rec(postorder, left, i) && rec(postorder, i + 1, right - 1);
    }

    bool verifyPostorder(vector<int>& postorder) {
        return rec(postorder, 0, postorder.size() - 1);
    }
};

二叉树相关问题考虑递归

  • 子问题显然是左右子树是否都是二叉搜索树,递归传入的参数则是该子树的后序遍历序列,left和right标明该序列的范围
  • 结束条件首先想到left == right,只有一个节点时肯定是二叉搜索树。按照我写的left和right在递归中的变化方式不会出现left > right的情况。但还要考虑特殊情况,当传入空序列时left = 0, right = -1,所以终止条件应为left >= right
  • 一轮递归的处理逻辑为:切分左右子树的后序遍历序列->验证左右子树节点是否均小于/大于当前根->递归验证左右子树是否为二叉搜索树

切分子树序列的做法是从Postorder的末尾元素(当前根)向前查找,遇到小于当前根的就停下,用i记录下标。则[left,i]是左子树序列,[i+1,right-1]是右子树序列。基于这个查找过程,划分出的右子树天然就满足大于当前根,只需验证左子树节点是否均小于根即可。

解2

class Solution {
public:
    bool verifyPostorder(vector<int>& postorder) {
        int n = postorder.size(), tmp = INT_MAX;
        stack<int> st;
        for(int i = n - 1; i >= 0; i--){
            if(st.empty() || postorder[i] > st.top()){
                if(postorder[i] < tmp){
                    st.push(postorder[i]);
                }
                else{
                    return false;
                }
            }
            else{
                while(!st.empty() && st.top() > postorder[i]){
                    tmp = st.top();
                    st.pop();
                }
                st.push(postorder[i]);
            }
        }
        return true;
    }
};

后序遍历的倒序为 “根、右、左” 顺序。
设后序遍历倒序列表为 [rn,rn1,...,r1][r_{n}, r_{n-1},...,r_1],遍历此列表,设索引为 i ,若为二叉搜索树 ,则有:

  • 当节点值 ri>ri+1r_i > r_{i+1} 时: 节点 rir_i 一定是节点 ri+1r_{i+1} 的右子节点。
  • 当节点值 ri<ri+1r_i < r_{i+1} 时: 节点 rir_i 一定是某节点 root 的左子节点,且 root 为节点 ri+1,ri+2,...,rnr_{i+1}, r_{i+2},..., r_{n} 中值大于且最接近 rir_i 的节点(∵ root 直接连接左子节点 rir_i) 当遍历时遇到递减节点 ri<ri+1r_i < r_{i+1},若为二叉搜索树,则对于后序遍历倒序序列中节点 rir_i 右边的任意节点 rx[ri1,ri2,...,r1]r_x \in [r_{i-1}, r_{i-2}, ..., r_1] ,必有节点值 rx<rootr_x < root

节点 rxr_x 只可能为以下两种情况:① rxr_xrir_i 的左、右子树的各节点;② rxr_x 为 root 的父节点或更高层父节点的左子树的各节点。在二叉搜索树中,以上节点都应小于 root。
如图所示:rxr_x的所有可能位置

image.png
遍历 “后序遍历的倒序” 会多次遇到递减节点 rir_i ,若所有的递减节点 rir_i 对应的父节点 root 都满足以上条件,则可判定为二叉搜索树。

根据以上特点,考虑借助 单调栈 实现: 借助一个单调栈 stack 存储值递增的节点; 每当遇到值递减的节点 rir_i ,则通过出栈来更新节点 rir_i 的父节点 root ; 每轮判断 rir_i 和 root 的值关系: 若 ri>rootr_i > root则说明不满足二叉搜索树定义,直接返回 false 。 若 ri<rootr_i < root 则说明满足二叉搜索树定义,则继续遍历。

  • 在判断条件中需要取st.top()时一定要把判断栈空写在第一个条件

题目15

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量)。

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        while(n){
            if(n & 1){
                count++;
            }
            n >>= 1;
        }
        return count;
    }
};

就是快速幂简化版,模板题

题目65

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

class Solution {
public:
    int add(int a, int b) {
        while(b != 0){
            int c = (unsigned)(a & b) << 1; //当前位的进位,左移一位下一轮与下一位的本位运算
            a ^= b; //所有位的非进位结果都存在a了
            b = c; //把c赋值给b,下一轮a^b的时候实际上就是计算下一位的本位结果与当前位的进位结果  
        } 
        return a;
    }
};

本题是用位运算代替加法,模板题。

先看一下十进制:

从低位开始按位相加,逢10进一,累加到高位,依次进行得到最后结果。
我们把 每一位 加法的输出分为:本位 和 进位

     6                3
 +   8             +  4
--------         --------
  1  4             0  7
进位 本位        进位 本位

4 是本位         7 是本位
1 是进位         0 是进位

本位/进位 的处理:先以十进制为例

22 + 89 = 111

  百 十 个
     2  2
   + 8  9
   --------
     0  1 // 本位
     1  1 // 进位
------------------------------------------------
      0  1
 +    1  0  //10就是最低位的进位1 * 位权10得来
 ----------
      1  1
      
      1  1
 + 1  0  0  //同理100是下一位的进位1*位权100得到
 ----------
   1  1  1

01 是计算后的本位,直接记录。
11 是计算后的进位,需要做 进位逻辑 后进入下轮计算。(进位逻辑:乘以进制数,也就是 11 * 10)
而这里没有下轮计算了,最终得到 111

然后类比到二进制:

 a ^ b // 计算出2个加数二进制下每一位的本位
 a & b // 计算出2个加数二进制下当前位的进位

 (a & b) << 1 // 进位做进位逻辑,也就是 * 2

在下一轮循环中由下一位的本位与当前位的进位继续计算下一位的本位与进位,重复直至某一位无进位

  • 注意int c = (unsigned)(a & b) << 1; c++不支持负数左移,所以必须要转成unsigned再左移

题目56(I)

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        int x = 0;
        for(int i : nums){
            x ^= i;
        }
        int y = x & (~x + 1), a = 0, b = 0;
        
        for(int i : nums){
            if((i & y) == 0){
                a ^= i;
            }
            else{
                b ^= i;
            }
        }

        return vector<int>{a, b} ;
    }
};

让我们先来考虑一个比较简单的问题:

如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

答案很简单:全员进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 0,不同结果为 1。那么在计算过程中,成对出现的数字的所有位会两两抵消为 0,最终得到的结果就是那个出现了一次的数字。

那么这一方法如何扩展到找出两个出现一次的数字呢?

如果我们可以把所有数字分成两组,使得:

1.两个只出现一次的数字在不同的组中;
2.相同的数字会被分到相同的组中。

那么对两个组分别进行异或操作,即可得到答案的两个数字。这是解决这个问题的关键。

那么如何实现这样的分组呢?

记这两个只出现了一次的数字为 a 和 b,那么所有数字异或的结果就等于 a 和 b 异或的结果,我们记为x。
如果我们把 x 写成二进制的形式 xkxk1x2x1x0x_k x_{k - 1} \cdots x_2 x_1 x_0,其中 xi{0,1}x_i \in \{ 0, 1 \},我们考虑一下 xi=0x_i = 0xi=1x_i = 1 的含义是什么?
它意味着如果我们把 a 和 b 写成二进制的形式,aia_ibib_i 的关系——xi=1x_i = 1 表示 aia_ibib_i 不等,xi=0x_i = 0 表示 aia_ibib_i 相等。假如我们任选一个不为 0 的 xix_i,按照第 i 位给原来的序列分组,如果该位为 0 就分到第一组,否则就分到第二组,这样就能满足以上两个条件。

int y = x & (~x + 1)是取x最右侧的1,~x + 1就是x的补码,原码与补码以最右侧的1为分界线,右侧相同,左侧相反。那么他们两个按位与就得到了最右侧的的1。

另外,题目要求空间复杂度O(1),所以我们不能真的新定义两个vector存放两组数字,那样就O(n)了。直接遍历原数组,让逻辑上同一组的数字异或即可。