24秋招算法笔记

0 前言


1 数组

1.1 在排序数组中查找元素的第一个和最后一个位置

题目链接

  • 思路
    二分查找分多种情况,主要为查找区间的不同(左闭右闭左闭右开等),会影响到循环判断的结束条件、左值右值的更新、mid的取值。
    本题可以用两次二分查找解决。其中两次的二分查找的查找区间分别为左闭右开左开右闭,需要注意的是,在左开右闭的区间查找,mid的取值为 (left + right + 1) / 2,即为向上取整。
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        if(n == 0) return {-1, -1};
        // 找最左边和最右边出现的地方
        int ansl = -1, ansr = -1, left = 0, right = n - 1;
        // 第一次左闭右开
        while(left < right) {
            int mid = (left + right) / 2;
            if(nums[mid] >= target) {
                if(nums[mid] == target) {
                    ansl = mid;
                }
                right = mid;
            } else {
                left = mid + 1;
            } 
        }
        if(ansl == -1) {
            if(nums[n-1] == target) {
                return {n - 1, n - 1};
            } else {
                return {-1, -1};
            }
        }
        left = 0, right = n - 1;
        第二次左开右闭
        while(left < right) {
            int mid = (left + right + 1) / 2;
            if(nums[mid] > target) {
                right = mid - 1;
            } else {
                if(nums[mid] == target) {
                    ansr = mid;
                }
                left = mid;
            }
        }
        if(ansr == -1) return {0, 0};
        else return {ansl, ansr};
    }
};

1.2 移除元素

题目链接

  • 思路
    在要求数组进行原地删除元素时(在数组中添加和删除元素的时间复杂度均为O(n)),可以考虑使用双指针,用快指针遍历整个数组,用慢指针维护结果数据。(类似题目:移动零
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int fast = 0, slow = 0;
        int len = nums.size();
        while(fast < len){
            if(nums[fast] != val){
                nums[slow] = nums[fast];
                slow++;
            }
            fast++;
        }
        return slow;
    }
};

1.3 长度最小的子数组

题目链接

  • 思路
    滑动窗口。这里直接引用 代码随想录 中对滑动窗口的解释:所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。在使用滑动窗口,有三点需要注意:1、窗口内是什么?2、如何移动窗口的起始位置?3、如何移动窗口的结束位置?
    本题中,窗口内是什么:连续的子数组,并且其总和大于等于target ;窗口的起始位置如何移动:如果当前窗口的值大于等于target 了,起始位置就要向前移动了;窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。(类似题目:水果成篮
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size();
        int ans = n + 1, sum = 0, count = 0;
        for(int i = 0, j = 0; j < n; ++ j) {
            sum += nums[j];
            ++ count;
            while(sum >= target) {
                ans = min(ans, count);
                sum -= nums[i];
                ++ i;
                -- count;
            }
        }
        return ans == n + 1 ? 0 : ans;
    }
};

1.4 区间和

题目链接 (题目来自卡码网KamaCoder, 使用的是ACM模式)

  • 思路
    使用 前缀和 可以避免重复的加法计算。博主说:C++ 代码面对大量数据的读取和输出操作,最好用scanfprintf,耗时会小很多。(类似题目:开发商购买土地
#include <iostream>
#include <vector>
  
int main() {
    int n = 0, a, b, p = 0;
    scanf("%d", &n);
    vector<int> preSum(n);
    for(int i = 0; i < n; ++ i) {
        int t;
        scanf("%d", &t);
        p += t;
        preSum[i] = p;
    }
    while(~scanf("%d%d", &a, &b)) {
        if(a == 0) {
            printf("%d\n", preSum[b]);
        } else {
            printf("%d\n", preSum[b]- preSum[a - 1]);
        }
    }
    return 0;
}

1.5 找出分区值(2024.7.26每日一题)

题目链接

  • 思路
    对于第一眼没有思路的数组题目可以考虑先排序, 排完可能就有了
class Solution {
public:
    int findValueOfPartition(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int ans = nums[1] - nums[0];
        for(int i = 1; i < nums.size() - 1; ++ i) {
            int sub = nums[i + 1] - nums[i];
            ans = ans < sub ? ans : sub;
        }
        return ans;
        
    }
};

2 链表

2.0 定义链表

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

2.1 移除链表元素

题目链接

  • 思路
    主要说明一个思想:对于链表题目都可以考虑添加一个虚拟节点 dummyHead 来简化操作。
    本题中,在移除某个节点时(头节点除外),都是通过前一个结点来移除当前节点的,而头节点没有前一个节点,因此这里引入一个虚拟节点 dummyHead ,使得移除头节点的操作更加方便。
    在链表题目中,通常会有很多节点的移动操作,这个过程容易混乱,指针指着指着指哪里去了都不知道,可以手动画一个链表示意图方便理解(这很好用)。
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode *dummyHead = new ListNode(0, head);
        ListNode *p = dummyHead;
        while(p->next != nullptr) {
            if(p->next->val == val) {
                p->next = p->next->next;
            } else {
                p = p->next;
            }
        }
        return dummyHead->next;
    }
};

2.2 反转链表

题目链接

  • 思路
    本题有两种方法:
    头插法 ,简单来说就是定义一个虚拟头节点 dummyHead ,然后在遍历链表的同时,把遍历到的节点插入到 dummyHead 后面,其中的难点在于节点断开和节点接入容易产生逻辑混乱,建议画图理解。
    双指针法 (很妙的想法,可以学习)
  • 头插法代码
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* p = head;
        while(p != nullptr) {
            ListNode* tmp = dummyHead->next;
            dummyHead->next = p;
            p = p->next;
            dummyHead->next->next = tmp;

        }
        return dummyHead->next;
    }
};
  • 双指针法代码
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *pre = nullptr, *curr = head;
        while(curr != nullptr) {
            ListNode *tmp = curr->next;
            curr->next = pre;
            pre = curr;
            curr = tmp;
        }
        return pre;
    }
};

2.3 两两交换链表中的节点

题目链接

  • 思路
    主要是在断开和接入节点时容易发生混乱,画图是个好方法。
    还有一个好方法,来自龙之笔记 ,就是多定义变量,可以简化很多操作,以下代码就是这样的思路。
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode *dummy_head = new ListNode(-1, head);
        ListNode *p = dummy_head;
        while (p->next != NULL && p->next->next != NULL) {
            // a和b为要进行交换的节点,c为后面的节点
            ListNode *a = p->next;
            ListNode *b = a->next;
            ListNode *c = b->next;

            // 将a和b从原链表中断开(为了方便理解,可省略)
            p->next = NULL;
            a->next = NULL;
            b->next = NULL;

            // 重新拼接
            p->next = b;
            b->next = a;
            a->next = c;

            p = a;
        }   
        return dummy_head->next;
    }
};


2.4 删除链表的倒数第N个节点

题目链接

  • 思路
    使用 快慢指针 ,即让快指针先跑n步,然后慢指针和快指针一起往前跑,当快指针跑到最后一个节点时,此时慢指针所在节点即为要删除的节点的前一个节点。注意,当要删除某个节点时,要从在前一个节点操作。
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // if(head->next == nullptr) return nullptr;
        ListNode* dummy = new ListNode(0, head);
        ListNode* fast = dummy;
        ListNode* slow = dummy;
        for(int i = 0; i <= n; i++){
            fast  = fast->next;
        }
        while(fast != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next;
        return dummy->next;
    }
};

2.5 链表相交

题目链接

  • 思路
    首先判断两个链表是否有交点,如果相交,则两个链表的后 N (N >= 1) 个节点均为同一个节点,判断两个链表的尾节点是否为同一个节点即可,同时并记录两个链表的长度 numAnumB 。此时需要让两个链表的长度“相等”,让较长链表往前走 |numA - numB| 步,再同时遍历两个链表,当遍历到的节点为同一个时,即为所求。
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *pa = headA, *pb= headB;
        if(pa == NULL || pb == NULL) return NULL;
        int numA = 0, numB = 0;
        while(pa->next != NULL) {
            pa = pa->next;
            ++ numA;
        }
        while(pb->next != NULL) {
            pb = pb->next;
            ++ numB;
        }
        if(pa != pb) return NULL; // 尾节点不是同一个,不相交
        pa = headA, pb= headB;
        if(numA > numB) {
            for(int i = 0; i < numA - numB; ++ i) {
                pa = pa->next;
            }
        } else {
            for(int i = 0; i < numB - numA; ++ i) {
                pb = pb->next;
            }
        }
        while(pa != pb) {
            pa = pa->next;
            pb = pb->next;
        } 
        return pa;
    }
};

2.6 环形链表II

题目链接

  • 思路
    遍历链表,并用一个 set 存储节点,目的是为了判断是否出现重复节点,当节点重复,则说明找到了环,当遍历到 NULL 时,则说明无环。
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *p = head;
        unordered_set<ListNode*>  s;
        while(p != NULL) {
            if(s.find(p) == s.end()) {
                s.insert(p);
            } else {
                break;
            }
            p = p->next;
        }
        return p;
    }
};

有另一中更巧妙的方法


3 哈希表

3.1 有效的字母异位词

题目链接

  • 思路
    用一个长度为26的数组记录每个字母出现的字数,出现在 s 中则加1,出现在 t 中则减1,最后判断数组元素是否全为0即可。(数组本质上也是一种哈希表)
class Solution {
public:
    bool isAnagram(string s, string t) {
        if(s.size() != t.size()){
            return false;
        }
        vector<int> letter(26, 0);
        for(int i = 0; i < s.size(); i++){
            letter[s[i] - 'a']++;
            letter[t[i] - 'a']--;
        }
        for(int i = 0 ; i < 26; i++){
            if(letter[i] != 0){
                return false;
            }
        }
        return true;
    }
};

3.2 快乐数

题目链接

  • 思路
    可以用一个 map 把每次变换的数字进行保存,只需判断下一次变换的数字是否在 map 中即可。
    这里也可以用 快慢指针 的思路:如果要判断某个“链表”是否有环,可以使用 快慢指针 进行判断,快指针慢指针 一起出发, 快指针 一次走两步, 慢指针 一次走一步,如果链表有环,那么 快慢指针 终将相遇,如果无环,则 快指针 会先抵达终点。本题中,可以把所给数字比作链表,用 快慢指针 进行遍历,如果 快慢指针 的值相等,则说明有环;如果 快指针 的值为1,则说明无环,即为快乐数。
class Solution {
public:
    bool isHappy(int n) {
        int slow = int2happyNumber(n);
        int fast = int2happyNumber(slow);
        do {
            slow = int2happyNumber(slow);
            fast = int2happyNumber(int2happyNumber(fast));
            if(fast == 1) return true;
        } while(slow != fast);
        return false;
    }

    int int2happyNumber(int num) {
        int ret = 0;
        while(num != 0) {
            ret += (num % 10) * (num % 10);
            num /= 10;
        }
        return ret;
    }
};

3.3 有人相爱,有人夜里开车看海,有人LeetCode第一题都做不出来

题目链接

  • 思路
    定义一个 map 用于存储数值和下标,边遍历边找是否有符合条件的值,有则返回,无则把数值和下标写入 map 中。
    本题使用数据结构类型为 unordered_map , 原因为哈希表中的 key 不要求有序,并且查询操作较多,因此选择 unordered_map可以节省时间。 mapunordered_map 具体区别详见代码随想录
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashmap;
        for(int i = 0; i < nums.size(); i++){
            if(hashmap.count((target - nums[i]))){
                return {i, hashmap[target - nums[i]]};
            }
            hashmap[nums[i]] = i;
        }
        return {};
    }
};

3.4 四数相加 II

题目链接

  • 思路
    第一想到的是直接暴力求解,四层循环,时间复杂度为 O(n^4) ,果断超时。优化一层,可以把 num1map 保存,剩下三个数组使用三层循环遍历,时间复杂度为 O(n^3)(实际为 O(n^3) + O(n)) ,又超时。那继续优化,把 num1num2 中的数值两两相加,并用 map 保存,剩下两个数组使用二重循环遍历,时间复杂度为 O(n^2)(实际为 O(n^2) + O(n^2)) ,通过!(本题依旧使用 unordered_map ,原因是 key 可以无序,并且查询操作多。)
class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int, int> hashmap; // 存 nums1 + nums2 的出现的情况,key为数值, value为出现的次数
        int ans = 0, n = nums1.size();
        for(int i = 0; i < n; ++ i) {
            for(int j = 0; j < n; ++ j) {
                int add = nums1[i] + nums2[j];
                if(hashmap.count(add) == 0) {
                    hashmap[add] = 1;
                } else {
                    ++ hashmap[add];
                }
            }
        }
        for(int k = 0; k < n; ++ k) {
            for(int l = 0; l < n; ++ l) {
                int sub = 0 - nums3[k] - nums4[l];
                if(hashmap.count(sub) != 0) {
                    ans += hashmap[sub];
                }
            }
        }
        return ans;
    }
};

4 字符串

4.1 反转字符串

题目链接

  • 思路
    双指针 ,一个往前遍历,一个往后遍历,遍历的同时交换数值,即可达到空间复杂读为 O(1) 的效果。
class Solution {
public:
    void reverseString(vector<char>& s) {
        int left = 0, right = s.size() - 1;
        while(left < right) {
            auto tmp = s[left];
            s[left] = s[right];
            s[right] = tmp;
            left ++;
            right --;
        }
    }
};

4.2 反转字符串II

题目链接

  • 思路
    大致思路和上题一样,只需要关注 leftright 的位置即可。
class Solution {
public:
    string reverseStr(string s, int k) {
        for (int i = 0; i < s.size(); i += 2*k) {
            int left = i;
            int right = i + k - 1;
            if (right > s.size() - 1) {
                right = s.size() - 1;
            }
            while (left < right) {
                auto tmp = s[left];
                s[left] = s[right];
                s[right] = tmp;
                left ++;
                right --;
            }
        }
        return s;
    }
};

4.3 替换数字

题目链接

  • 思路
    遍历字符串,逐个判断字符。
#include <iostream>
using namespace std;
int main() {
    string s, ans;
    cin >> s;
    for(auto i : s) {
        if(i >= '0' && i <= '9') {
            ans += "number";
        } else {
            ans += i;
        }
    }
    cout << ans;
    return 0;
}

4.4 反转字符串中的单词

题目链接

  • 思路
    可以用 vector<string> 把字符串中的单词存起来,最后再拼接成一个题目所要求的字符串。
    (进阶)使用 O(1) 的空间,那就要在原地反转。原地反转的操作如下:先反转整个字符串,然后再逐个反转单词(这个过程可以举个例子手写模拟一下)。题目中会出现多余的空格,因此在反转操作前,要先移除多余的空格,如果使用 erase() 操作进行移除,时间复杂度会过高( erase() 操作本身就是 O(n) ),因此这里使用 双指针 移除多余空格,用 快指针 遍历整个字符串,用 慢指针 维护结果数据。
class Solution {
public:
    string reverseWords(string s) {
	    // 双指针法移除空格
        int fast = 0, slow = 0;
        char pre = ' ';
        while(fast < s.size()) {
            if(s[fast] != ' ' || pre != ' ') {
                s[slow] = s[fast];
                pre = s[slow];
                ++ slow;
            }
            ++ fast;
        }
        // 移除多余字符
        if(s[slow - 1] == ' ') {
            s = s.substr(0, slow - 1); // substr() 操作可以用 resize() 替代, 即 s.resize(slow - 1)
        } else {
            s = s.substr(0, slow);
        }
        // 先反转整个字符串
        reverse(s, 0, s.size() - 1);
        // 再逐个反转单词
        int left = 0, right = 0;
        for(; right < s.size(); ++ right) {
            if(s[right] == ' ') {
                reverse(s, left, right - 1);
                left = right + 1;
            }
        }
        reverse(s, left, s.size() - 1);
        return s;
    }

    void reverse(string& s, int left, int right) {
        while(left < right) {
            auto tmp = s[left];
            s[left] = s[right];
            s[right] = tmp;
            left ++;
            right --;
        }
    }
};

4.5 右旋字符串

题目链接

  • 思路
    可以通过字符串反转的方式实现。先反转前 len-k 个,再反转后 k 个,最后反转整个字符串。
#include <iostream>
using namespace std;

void reverse(string &s, int left, int right) {
    while(left < right) {
        auto tmp = s[left];
        s[left] = s[right];
        s[right] = tmp;
        ++ left;
        -- right;
    }
}

int main() {
    int k, len;
    string s;
    cin >> k >> s;
    len = s.size();
    reverse(s, 0, len - 1 - k);
    reverse(s, len - k, len - 1);
    reverse(s, 0, len - 1);
    cout << s << endl;;
    return 0;
}

5 栈与队列

5.1 用栈实现队列

题目链接

  • 思路
    维护两个栈 inout ,一个用于接收数据,一个用于输出数据。输入的数据直接存进 in 中,当需要输出数据时,如果 out 为空,则把输入栈 in 的数据“倒入” out 中,然后输出 out 中的 top 元素,以达到一个 先进后出 的效果。
class MyQueue {
public:
    MyQueue() {
    }
    
    void push(int x) {
        in.push(x);
    }
    
    int pop() {
        if(out.empty()) {
            while(!in.empty()) {
                out.push(in.top());
                in.pop();
            } 
        }
        int ret = out.top();
        out.pop();
        return ret;
    }
    
    int peek() {
        if(out.empty()) {
            while(!in.empty()) {
                out.push(in.top());
                in.pop();
            }
        }
        return out.top();
    }
    
    bool empty() {
        return in.empty() && out.empty();
    }
private:
    stack<int> in, out;
};

5.2 用队列实现栈

题目链接

  • 思路
    其实使用单队列就可以解决。需要维护一个队列和队列长度 len 。当进行 pop 操作时,需要 len 来定位 pop 的元素的位置,这里的做法是,把队列中前 len - 1 个元素先出队再入队,那么此时队列的第一个元素就是需要 pop 的元素。实现 top 同理。
class MyStack {
public:
    MyStack() {
    }
    
    void push(int x) {
        que.push(x);
        ++ len;
    }
    
    int pop() {
        for(int i = 0; i < len - 1; ++ i) {
            que.push(que.front());
            que.pop();
        }
        -- len;
        int ret = que.front();
        que.pop();
        return ret;
    }
    
    int top() {
        for(int i = 0; i < len - 1; ++ i) {
            que.push(que.front());
            que.pop();
        }
        que.push(que.front());
        int ret = que.front();
        que.pop();
        return ret;
    }
    
    bool empty() {
        return len == 0;
    }
private:
    queue<int> que;
    int len = 0;
};

5.3 有效的括号

题目链接

  • 思路
    维护一个栈用于保存没有消掉的左括号,遇到匹配的右括号则消除,不匹配则返回 false
class Solution {
public:
    bool isValid(string s) { 
        if(s.size() % 2 == 1) return false;
        stack<char> stk;
        for(auto i : s){
            if(i == '(' || i == '{' || i == '[') {
                stk.push(i);
            } else {
                if(stk.size() == 0) {
                    return false;
                } else {
                    if(abs(stk.top() - i) > 2) return false;
                    // 字符运算 '(' - ')' = 1, '[' - ']' = 2, '{' - '}' = 2
                    else stk.pop();
                }
            }
        }
        return stk.empty();
    }
};

5.4 删除字符串中的所有相邻重复项

题目链接

  • 思路
    和上题类似。
class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> st;
        for(auto i : s) {
            if (st.empty() || i != st.top()) {
                st.push(i);
            } else {
                st.pop();
            }
        }
        string ans;
        while(!st.empty()) {
            ans += st.top();
            st.pop();
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};

5.5 逆波兰表达式求值

题目链接

  • 思路
    stack 维护数值,遍历 tokens,遇到数值就入栈,如果遇到运算符,则把栈顶两个元素取出运算,再把运算结果送回栈中。结果返回栈顶元素。
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> stk;
        for(auto i : tokens) {
            if (i == "+" || i == "-" || i == "*" || i == "/") {
                int num2 = stk.top();
                stk.pop();
                int num1 = stk.top();
                stk.pop();
                int result;
                if (i == "+") result = num1 + num2;
                else if (i == "-") result = num1 - num2;
                else if (i == "*") result = num1 * num2;
                else if (i == "/") result = num1 / num2;
                stk.push((result));
            } else {
                stk.push(stoi(i));
            }
        }
        return stk.top();
    }
};

5.6 滑动窗口最大值

题目链接

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        deque<int> dq(1, nums[0]);
        int n = nums.size(), m = -10000001;
        k = k < n ? k : n;
        for(int i = 1; i < k; ++ i) {
            while (!dq.empty() && nums[i] > dq.back()) {
                dq.pop_back();
            }
            dq.push_back(nums[i]);
        }
        ans.push_back(dq.front());
        for(int left = 0, right = k; right < n; ++ left, ++ right) {
            if (dq.front() == nums[left]) {
                dq.pop_front();
            }
            while (!dq.empty() && nums[right] > dq.back()) {
                dq.pop_back();
            }
            dq.push_back(nums[right]);
            ans.push_back(dq.front());
        }
        return ans;
    }
};

5.7 前K个高频元素

题目链接

  • 思路
    代码随想录 把这题归到栈于队列中了,但是我也妹用到栈和队列啊。先说说我的想法,用一个 map 存数值和出现的频率,然后对频率进行排序,最后返回前的 k 个元素。代码如下。其中,使用 sort() 函数排序时,使用的是自己定义的比较函数 cmp ,可以通过传递 cmp 作为第三个参数来实现。比较函数应该接受两个参数并返回一个布尔值,指示第一个参数是否应该排在第二个参数之前。
class Solution {
public:
    static bool cmp(pair<int,int> a, pair<int,int> b) {
        return a.second > b.second;
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
        map<int, int> hashmap;
        for (auto i : nums) {
            hashmap[i] ++;
        }
        vector<pair<int, int>> vec;
        for (auto it = hashmap.begin(); it != hashmap.end(); ++ it) {
            vec.push_back(pair<int, int>(it->first,it->second));
        }
        sort(vec.begin(), vec.end(), cmp);
        vector<int> ans;
        for(int i = 0; i < k; ++ i) {
            ans.push_back(vec[i].first);
        }
        return ans;
    }
};
  • 法2 :优先队列
    本质上是通过优先队列实现 对于输出最大(最小)k 个元素有奇效。这里的思路和上面的思路有相似之处,但是在最后找到 k 个元素的地方有很大优化。
class Solution {
public:
    // 小顶堆
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 要统计元素出现频率
        unordered_map<int, int> map; // map<nums[i],对应出现的次数>
        for (int i = 0; i < nums.size(); i++) {
            map[nums[i]]++;
        }

        // 对频率排序
        // 定义一个小顶堆,大小为k
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

        // 用固定大小为k的小顶堆,扫面所有频率的数值
        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
            pri_que.push(*it);
            if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                pri_que.pop();
            }
        }

        // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> result(k);
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pri_que.top().first;
            pri_que.pop();
        }
        return result;

    }
};

6 二叉树

6.0 定义二叉树

// 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) {}
};

6.1 二叉树的递归遍历

  • 写递归都要确定三要素:1、确定递归函数的参数和返回值; 2、确定终止条件; 3、确定单层递归的逻辑。二叉树的递归遍历主要有三种:前序遍历中序遍历后序遍历 ,遍历的顺序分别为:中左右左中右左右中 。以下是一段用递归 前序遍历 的代码实现。(题目链接
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> tree;
        preOrder(root, tree);
        return tree;
    }
    void preOrder(TreeNode* root, vector<int>& tree) {
        if(root == nullptr) return;
        tree.push_back(root->val); // 中序遍历就是把这行代码放在下一行,后序遍历是把这行代码放在下两行,
        preOrder(root->left, tree);
        preOrder(root->right, tree);
    }
};

6.2 二叉树的迭代遍历

  • 思路
    二叉树的迭代遍历本质上是模拟递归栈的过程。 前序遍历 是通过维护一个栈,每次从栈中获取节点并访问,同时将该节点的右孩子和左孩子(注意顺序)压入栈中,当栈为空时结束。而 后序遍历 有类似的过程,不同的在与将孩子节点压入栈中时,是先左孩子再右孩子,最后还需要将结果反转( reverse() )。
    二叉树的中序迭代遍历就相对麻烦一些,不止要维护一个栈用于模拟递归的过程,还需要维护一个指针当作信号一样,控制入栈和遍历操作。
  • 前序遍历
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        if(!root) return {};
        vector<int> ans;
        stack<TreeNode*> stk;
        stk.push(root);
        while (!stk.empty()) {
            TreeNode* cur = stk.top();
            ans.push_back(cur->val);
            stk.pop();
            if(cur->right != nullptr) stk.push(cur->right);
            if(cur->left != nullptr) stk.push(cur->left);
        }
        return ans;
    }
};
  • 后序遍历
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        if(!root) return {};
        vector<int> ans;
        stack<TreeNode*> stk;
        stk.push(root);
        while (!stk.empty()) {
            TreeNode* cur = stk.top();
            ans.push_back(cur->val);
            stk.pop();
            if(cur->left != nullptr) stk.push(cur->left);
            if(cur->right != nullptr) stk.push(cur->right);
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};

  • 中序遍历
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        if(!root) return {};
        vector<int> ans;
        stack<TreeNode*> stk;
        TreeNode* cur = root;
        while(cur != nullptr || !stk.empty()) {
            if(cur != nullptr) {
                stk.push(cur);
                cur = cur->left;
            } else {
                cur = stk.top();
                stk.pop();
                ans.push_back(cur->val);
                cur = cur->right;
            }
        }
        return ans;
    }
};

6.3 二叉树的层序遍历

题目链接

  • 思路
    配合队列的 先进先出 的特性实现层序遍历。先将根节点入队, 每次从队列中获取节点并访问,同时将该节点的左右节点放进队列中。
    插一嘴(), 二叉树的层序遍历是一种 广度优先(BFS) 的遍历方式,而上问提到的前中后序遍历,都是 深度优先(DFS) 的遍历方式。
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        deque<TreeNode*> dq;
        if(root != nullptr) dq.push_back(root);
        vector<vector<int>> ans;
        while(!dq.empty()) {
            int len = dq.size();
            vector<int> level;
            for(int i = 0; i < len; ++ i) {
                TreeNode* node = dq.front();
                dq.pop_front();
                level.push_back(node->val);
                if(node->left != nullptr) dq.push_back(node->left);
                if(node->right != nullptr) dq.push_back(node->right);
            }
            ans.push_back(level);
        }
        return ans;
    }
};

6.4 对称二叉树

题目链接
-思路
硬判断,用 if 遍历所有情况。写递归思路要清晰。

class Solution {
public:
    bool check(TreeNode* left, TreeNode* right) {
        if(left == nullptr && right == nullptr) return true;
        else if(left == nullptr && right != nullptr) return false;
        else if(left != nullptr && right == nullptr) return false;
        else if(left->val != right->val) return false;
        else return check(left->left, right->right) && check(left->right, right->left);
    }

    bool isSymmetric(TreeNode* root) {
        return root == nullptr ? true : check(root->left, root->right);
    }
};

6.5 平衡二叉树

题目链接

  • 思路
    有几个点需要注意,二叉树的高度和深度是不一样的概念,高度是从下往上数,深度是从上往下数。本题使用了一个小技巧,就是把一些特殊情况进行标记。函数 getHeigh(TreeNode* node) 本意是获取树的高度,但是这里使用了 -1 当作不平衡的标记,当树平衡时会返回树的高度,当不平衡时树的高度就无所谓了,此时返回 -1
class Solution {
public:
    int getHeigh(TreeNode* node) {
        if(node == nullptr) return 0;
        int leftH = getHeigh(node->left);
        int rightH = getHeigh(node->right);
        if(leftH != -1 && rightH != -1 && abs(leftH - rightH) <= 1) return leftH > rightH ? leftH +1 : rightH + 1;
        else return -1;
    }

    bool isBalanced(TreeNode* root) {
        if(getHeigh(root) != -1) return true;
        else return false;
    }
};

6.6 二叉树的所有路径

题目链接

  • 思路
    从上遍历到下,当到叶子节点时,就遍历完了一条路径。把要返回的字符串数组设置为全局变量(也可以不使用全局变量,把该字符串数组当作参数传入函数也行),以便直接记录路径。
class Solution {
public:
    void travel(TreeNode* node, string s) {
        s += to_string(node->val);
        if(node->left == nullptr && node->right == nullptr) {
            ret.push_back(s);
            return ;
        }
        s += "->";
        if(node->left) travel(node->left, s);
        if(node->right) travel(node->right, s);
    }
    
    vector<string> binaryTreePaths(TreeNode* root) {
        string s;
        travel(root, s);
        return ret;
    }
private:
    vector<string> ret;
};

6.7 从中序与后序遍历序列构造二叉树

题目链接

  • 思路
    看看视频挺好的《代码随想录》算法视频公开课。但是问题又来了,为什么我写出来的代码效率比这么差,耗时又长内存占用又大(摊手,What can I say)。
    (内存占用大是因为每层遍历都用了新的 vector 。优化:使用题目所给的数组初始化新的数组)
class Solution {
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int len = postorder.size();
        if(len == 0) return nullptr;
        int rootVal = postorder[len - 1];
        TreeNode* root = new TreeNode(rootVal);
        vector<int> inLeft, postLeft, inRight, postRight;
        int index = 0;
        while(index < len) {
            if(inorder[index] != rootVal) {
                inLeft.push_back(inorder[index]);
                postLeft.push_back(postorder[index]);
                ++ index;
            } else {
                break;
            }
        }
        while(index + 1< len) {
            inRight.push_back(inorder[index + 1]);
            postRight.push_back(postorder[index]);
            ++ index;
        }
        root->left = buildTree(inLeft, postLeft);
        root->right = buildTree(inRight, postRight);
        return root;
    }
};

6.8 合并二叉树

题目链接

  • 思路
    递归就要多写,多写了就会。
class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if(root1 == nullptr && root2 == nullptr) return nullptr;
        if(root1 == nullptr) return root2;
        else if(root2 == nullptr) return root1;
        root1->val += root2->val;
        root1->left = mergeTrees(root1->left, root2->left);
        root1->right = mergeTrees(root1->right, root2->right);
        return root1;
    }
};

6.9 验证二叉搜索树

题目链接

  • 思路
    有一个妙手,中序遍历 的遍历顺序为 左中右 ,正好符合二叉搜索树的特性。因此用中序遍历二叉树,对遍历的结果进行判断,如果严格递增,则为二叉搜索树。(By the way,记得复习 中序遍历 的迭代写法 -> 代码随想录
class Solution {
public:
    void preOrder(TreeNode* root, vector<int>& vec) {
        if(root->left != nullptr) preOrder(root->left, vec);
        vec.push_back(root->val);
        if(root->right != nullptr) preOrder(root->right, vec);
    }

    bool isValidBST(TreeNode* root) {
        vector<int> vec;
        preOrder(root, vec);
        int pre = vec[0];
        for(int i = 1; i < vec.size(); ++ i) {
            if(vec[i] > pre) {
                pre = vec[i];
            } else {
                return false;
            }
        }
        return true;
    }
};

6.10 删除二叉搜索树中的节点

题目链接

  • 思路
    涉及链式节点的删除,通常要使用到该节点的前一个节点。但是用什么值进行替换呢,两种值有效:一是该节点的左子树中的最大值,也就是该节点左子树中最右边的值,二是该节点右子树中的最小值。
    (请欣赏💩山)(太辣眼了,还是看看 代码随想录的代码 吧)
class Solution {
public:
    vector<TreeNode*> findNode(vector<TreeNode*> nodes, int key) {
        if(nodes[1] == nullptr) return nodes;
        if(nodes[1]->val == key) return nodes;
        else if(nodes[1]->val > key) return findNode({nodes[1], nodes[1]->left}, key);
        else if(nodes[1]->val < key) return findNode({nodes[1], nodes[1]->right}, key);
        return nodes;
    }
    TreeNode* deleteNode(TreeNode* root, int key) {
        TreeNode* dummpNode = new TreeNode(0, root, nullptr);
        vector<TreeNode*> nodes = findNode({dummpNode, root}, key);
        // node[1]为删除的节点,node[0]为要删除节点的前一个节点
        if(nodes[1] == nullptr) return root;
        else {
            if(nodes[1]->left == nullptr && nodes[1]->right == nullptr) {
                if(nodes[0]->left == nodes[1]) nodes[0]->left = nullptr;
                else nodes[0]->right = nullptr;
            } else if(nodes[1]->left != nullptr) {
                TreeNode *tmp = nodes[1]->left, *pre = nodes[1];
                if(tmp->right == nullptr) {
                    nodes[1]->val = tmp->val;
                    pre->left = tmp->left;
                }
                else {
                    while(tmp->right != nullptr) {
                        pre = tmp;
                        tmp = tmp->right;
                    }
                    nodes[1]->val = tmp->val;
                    pre->right = tmp->left;
                }
            } else if(nodes[1]->right != nullptr) {
                TreeNode *tmp = nodes[1]->right, *pre = nodes[1];
                if(tmp->left == nullptr) {
                    nodes[1]->val = tmp->val;
                    pre->right = tmp->right;
                } else {
                    while(tmp->left != nullptr) {
                        pre = tmp;
                        tmp = tmp->left;
                    }
                    nodes[1]->val = tmp->val;
                    pre->left = tmp->right;
                }
            }
        }
        return dummpNode->left;
    }
};

6.11 把二叉搜索树转化为搜索树

题目链接

  • 思路
    想到二叉搜索树,首先想到 中序遍历 ,因为 中序遍历 的二叉搜索树结果是一个单调的数组。本题使用一次倒过来的 中序遍历 (右中左),边遍历边修改节点的值。(中序遍历的题都使用 迭代法 写,熟悉一下 迭代法 的写法)
class Solution {
public:
    TreeNode* convertBST(TreeNode* root) {
        int sum = 0;
        // 用一个倒过来的中序遍历
        stack<TreeNode*> st;
        if(root) st.push(root);
        while(!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            if(node != nullptr) {
                if(node->left) st.push(node->left);
                st.push(node);
                st.push(nullptr);
                if(node->right) st.push(node->right);
            } else {
                node = st.top();
                st.pop();
                sum += node->val;
                node->val = sum;
            }
        }
        return root;
    }
};

7 回溯(sù)

7.0 回溯模板

来自代码随想录

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

7.1 组合

题目链接

  • 思路
    主要参考 7.0 中提到的回溯模板,根据模板一点一点往里面填东西即可。其实还有可以优化的地方,就是在回溯中的 for 循环中,可以将不必要的循环移除,称为 剪枝 。本体中,可以将 i <= n 优化成 i <= n - k + v.size() + 1 (优化的过程可以好好想一想, n - k + v.size() + 1 这个值是怎么来的)。
class Solution {
public:
    void backtracking(vector<vector<int>>& ans, vector<int>& v, int n, int k, int next) {
        if(v.size() == k) {
            ans.push_back(v);
            return;
        }
        for(int i = next; i <= n; ++i) {
            v.push_back(i);
            backtracking(ans, v, n, k, i + 1);
            v.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> ans;
        vector<int> v;
        backtracking(ans, v, n, k, 1);
        return ans;
    }
};

7.2 组合总和III

题目链接

  • 思路
    套模板。本题的 剪枝 主要在两个方面,一是总和 n , 如果已经累加的数已经超过 n 了,则直接 return ,二是个数 k ,和上题一样,在循环中,把 i <= 9 优化成 i <= 9 - k + path.size() + 1
class Solution {
public:
    int sum = 0;
    vector<int> path;
    vector<vector<int>> result;
    void backTracking(int k, int n, int startNum) {
        if(sum > n) return;
        else if(sum == n) {
            if(path.size() == k) result.push_back(path);
            return;
        } else {
            if(path.size() >= k) return;
            else {
                for(int i = startNum; i <= 9 - (k - path.size()) + 1; ++ i) {
                    sum += i;
                    path.push_back(i);
                    backTracking(k, n, i + 1);
                    sum -= i;
                    path.pop_back();
                }
            }
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTracking(k, n, 1);
        return result;
    }
};

7.3 电话号码的字母组合

题目链接

  • 思路
    同样套模板,但是又不完全和上两题一样。总之 多做思考总结
class Solution {
public:
    unordered_map<char, string> dict = {
        {'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"}, 
        {'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
    };
    string path;
    vector<string> result;
    void backTracking(string digits, int startNum) {
        if(startNum == digits.size()) {
            result.push_back(path);
            return;
        }
        for(auto c : dict[digits[startNum]]) {
            path.push_back(c);
            backTracking(digits, startNum + 1);
            path.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits == "") return {};
        backTracking(digits, 0);
        return result;
    }
};

7.4 组合总和

题目链接

  • 思路
    什么时候适合用回溯?个人感觉要符合一下几点要求:1、要遍历所有情况 ; 2、遍历时循环的层数是根据输入的情况定的,不是限定好的(比如只有双层循环、三层循环等)。
    写回溯背模板,再根据模板填信息,就能过。
class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    int sum = 0;
    void backTracking(vector<int>& candidates, int target, int startIndex) {
        if(sum == target) {
            result.push_back(path);
            return ;
        } else if(sum > target) return;
        for(int i = startIndex; i < candidates.size(); ++ i) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backTracking(candidates, target, i);
            sum -= candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backTracking(candidates, target, 0);
        return result;
    }
};

7.5 复原IP地址

题目链接

  • 思路
    对于要求所给的字符串(或者数组)都用上的题,不需要写最外面一层的循环(即 for(int i = startIndex; i < s.size(); ++ i) )。本题和上面的电话组合就是如此。
class Solution {
public:
    vector<string> result;
    vector<string> path;
    void backTracking(string s, int startIndex) {
        if(path.size() == 4) {
            if(startIndex != s.size()) return ;
            else {
                result.push_back(path[0] + "." + path[1] + "." + path[2] + "." + path[3]);
                return;
            } 
        }
        for(int j = 1; j <= min(3, int(s.size() - startIndex)); ++ j) {
            string cur = s.substr(startIndex, j);
            if(cur[0] == '0' && cur != "0") continue;
            if(stoi(cur) > 255) continue;
            int rest = s.size() - startIndex - j;
            if(rest < 3 - path.size() || rest > (3 - path.size()) * 3) continue;
            path.push_back(cur);
            backTracking(s, startIndex + j);
            path.pop_back();
        }
    }
    vector<string> restoreIpAddresses(string s) {
        if(s.size() < 4 || s.size() > 12) return result;
        backTracking(s, 0);
        return result;
    }
};
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值