• LeetCode力扣刷题——妙用数据结构


    数据结构


    一、C++ STL

            在刷题时,我们几乎一定会用到各种数据结构来辅助我们解决问题,因此我们必须熟悉各种 数据结构的特点。C++ STL 提供的数据结构包括(实际底层细节可能因编译器而异):

    1. Sequence Containers:维持顺序的容器。

            (a). vector :动态数组,是我们最常使用的数据结构之一,用于 O ( 1 ) 的随机读取。因为大部分算法的时间复杂度都会大于 O ( n ) ,因此我们经常新建 vector 来存储各种数据或中间变量。因为在尾部增删的复杂度是 O ( 1 ) ,我们也可以把它当作 stack 来用。
            (b). list :双向链表,也可以当作 stack queue 来使用。由于 LeetCode 的题目多用 Node 来表示链表,且链表不支持快速随机读取,因此我们很少用到这个数据结构。一个例外是经典的 LRU 问题,我们需要利用链表的特性来解决,我们在后文会遇到这个问题。
            (c). deque :双端队列,这是一个非常强大的数据结构,既支持 O ( 1 ) 随机读取,又支持 O ( 1 )时间的头部增删和尾部增删,不过有一定的额外开销。
            (d). array :固定大小的数组,一般在刷题时我们不使用。
            (e). forward_list :单向链表,一般在刷题时我们不使用。

    2. Container Adaptors:基于其它容器实现的数据结构。

            (a). stack :栈,后入先出( LIFO )的数据结构,默认基于 deque 实现。 stack 常用于深度优先搜索、一些字符串匹配问题以及单调栈问题。
            (b). queue :队列,先入先出( FIFO )的数据结构,默认基于 deque 实现。 queue 常用于广度优先搜索。
            (c). priority_queue :优先队列,最大值先出的数据结构,默认基于 vector 实现堆结构。它可以在 O ( n log n ) 的时间排序数组,O ( log n ) 的时间插入任意值, O ( 1 ) 的时间获得最大值, O ( log n ) 的时间删除最大值。priority_queue 常用于维护数据结构并快速获取最大或最小值。

    3. Associative Containers:实现了排好序的数据结构。

            (a). set :有序集合,元素不可重复,底层实现默认为红黑树,即一种特殊的二叉查找树 (BST )。它可以在 O ( n log n ) 的时间排序数组, O ( log n ) 的时间插入、删除、查找任意值,O ( log n ) 的时间获得最小或最大值。这里注意, set priority_queue 都可以用于维护数据结构并快速获取最大最小值,但是它们的时间复杂度和功能略有区别,如 priority_queue 默认不支持删除任意值,而 set 获得最大或最小值的时间复杂度略高,具体使用哪个根据需求而定。
            (b). multiset :支持重复元素的 set
            (c). map :有序映射或有序表,在 set 的基础上加上映射关系,可以对每个元素 key 存一个值 value
            (d). multimap :支持重复元素的 map

    4. Unordered Associative Containers:对每个 Associative Containers 实现了哈希版本。

            (a). unordered_set :哈希集合,可以在 O ( 1 ) 的时间快速插入、查找、删除元素,常用于快速的查询一个元素是否在这个容器内。
            (b). unordered_multiset :支持重复元素的 unordered_set
            (c). unordered_map :哈希映射或哈希表,在 unordered_set 的基础上加上映射关系,可以对每一个元素 key 存一个值 value 。在某些情况下,如果 key 的范围已知且较小,我们也可以用 vector 代替 unordered_map ,用位置表示 key ,用每个位置的值表示 value
            (d). unordered_multimap :支持重复元素的 unordered_map

    二、经典问题

    1. 数组

            给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

            利用数组这种数据结构建立 n 个桶,把所有重复出现的位置进行标记,然后再遍历一遍数组,即可找到没有出现过的数字。进一步地,我们可以直接对原数组进行标记:把重复出现的数字在原数组出现的位置设为负数,最后仍然为正数的位置即为没有出现过的数。
    1. class Solution {
    2. public:
    3. vector<int> findDisappearedNumbers(vector<int>& nums) {
    4. vector<int> ans;
    5. for(const int & num: nums){
    6. int pos = abs(num) - 1;
    7. if(nums[pos] > 0){
    8. nums[pos] = -nums[pos];
    9. }
    10. }
    11. for(int i=0; isize(); ++i){
    12. if(nums[i] > 0){
    13. ans.push_back(i + 1);
    14. }
    15. }
    16. return ans;
    17. }
    18. };

    48. 旋转图像

            给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

            你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

            每次只考虑四个间隔 90 度的位置,可以进行 O ( 1 ) 额外空间的旋转。
    题目 48 - O(1) 空间旋转样例,相同颜色代表四个互相交换的位置

            对于矩阵中第 i 行的第 j 个元素,在旋转后,它出现在倒数第 i 列的第 j 个位置。由于矩阵中的行列从 0 开始计数,因此对于矩阵中的元素\textit{matrix}[\textit{row}][\textit{col}] ,在旋转后,它的新位置为\textit{matrix}_\textit{new}[\textit{col}][n - \textit{row} - 1]

    方法一:根据旋转前后的位置关系进行原地旋转。

    1. class Solution {
    2. public:
    3. void rotate(vectorint>>& matrix) {
    4. int temp = 0, n = matrix.size() - 1;
    5. for(int i=0; i<=n; ++i){
    6. for(int j=i; j
    7. temp = matrix[j][n - i];
    8. matrix[j][n - i] = matrix[i][j];
    9. matrix[i][j] = matrix[n - j][i];
    10. matrix[n - j][i] = matrix[n - i][n - j];
    11. matrix[n - i][n - j] = temp;
    12. }
    13. }
    14. }
    15. };

    方法二:用翻转操作代替旋转操作。

    1. class Solution {
    2. public:
    3. void rotate(vectorint>>& matrix) {
    4. int n = matrix.size() ;
    5. // 水平翻转
    6. for(int i=0; i2; ++i){
    7. for(int j=0; j
    8. swap(matrix[i][j], matrix[n - i - 1][j]);
    9. }
    10. }
    11. // 主对角线翻转
    12. for(int i=0; i
    13. for(int j=0; j
    14. swap(matrix[i][j], matrix[j][i]);
    15. }
    16. }
    17. }
    18. };

    240. 搜索二维矩阵 II

    240. Search a 2D Matrix II

            编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

            每行的元素从左到右升序排列
            每列的元素从上到下升序排列。

            这道题有一个简单的技巧:我们可以从右上角开始查找,若当前值大于待搜索值,我们向左移动一位;若当前值小于待搜索值,我们向下移动一位。如果最终移动到左下角时仍不等于待搜索值,则说明待搜索值不存在于矩阵中。
    1. class Solution {
    2. public:
    3. bool searchMatrix(vectorint>>& matrix, int target) {
    4. int m = matrix.size(), n = matrix[0].size();
    5. if(m == 0) return false;
    6. int i = 0, j = n - 1;
    7. while(i < m && j >= 0){
    8. if(matrix[i][j] == target){
    9. return true;
    10. }else if(matrix[i][j] > target){
    11. --j;
    12. }else{
    13. ++i;
    14. }
    15. }
    16. return false;
    17. }
    18. };

    769. 最多能完成排序的块

    769. Max Chunks To Make Sorted

            给定一个长度为 n 的整数数组 arr ,它表示在 [0, n - 1] 范围内的整数的排列。

            我们将 arr 分割成若干 块 (即分区),并对每个块单独排序。将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。

            返回数组能分成的最多块数量。

            从左往右遍历,同时记录当前的最大值,每当当前最大值等于数组位置时,我们可以多一次分割。
            为什么可以通过这个算法解决问题呢?如果当前最大值大于数组位置,则说明右边一定有小于数组位置的数字,需要把它也加入待排序的子数组;又因为数组只包含不重复的 0 n - 1 ,所以当前最大值一定不会小于数组位置。所以每当当前最大值等于数组位置时,假设为 p ,我们可以成功完成一次分割,并且其与上一次分割位置 q 之间的值一定是 q + 1 p 的所有数字。
    1. class Solution {
    2. public:
    3. int maxChunksToSorted(vector<int>& arr) {
    4. int chunks = 0, cur_max = 0;
    5. for(int i=0; isize(); ++i){
    6. cur_max = max(cur_max, arr[i]);
    7. if(cur_max == i){
    8. ++chunks;
    9. }
    10. }
    11. return chunks;
    12. }
    13. };

    2. 栈和队列

    232. 用栈实现队列

    232. Implement Queue using Stacks

            请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

    实现 MyQueue 类:

            void push(int x) 将元素 x 推到队列的末尾
            int pop() 从队列的开头移除并返回元素
            int peek() 返回队列开头的元素
            boolean empty() 如果队列为空,返回 true ;否则,返回 false
    说明:

            你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
            你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

            我们可以用两个栈来实现一个队列:因为我们需要得到先入先出的结果,所以必定要通过一个额外栈翻转一次数组。这个翻转过程既可以在插入时完成,也可以在取值时完成。
    1. class MyQueue {
    2. stack<int> in, out;
    3. public:
    4. MyQueue() {
    5. }
    6. void push(int x) {
    7. in.push(x);
    8. }
    9. int pop() {
    10. in2out();
    11. int x = out.top();
    12. out.pop();
    13. return x;
    14. }
    15. int peek() {
    16. in2out();
    17. return out.top();
    18. }
    19. void in2out(){
    20. if(out.empty()){
    21. while(!in.empty()){
    22. int x = in.top();
    23. in.pop();
    24. out.push(x);
    25. }
    26. }
    27. }
    28. bool empty() {
    29. return in.empty() && out.empty();
    30. }
    31. };
    32. /**
    33. * Your MyQueue object will be instantiated and called as such:
    34. * MyQueue* obj = new MyQueue();
    35. * obj->push(x);
    36. * int param_2 = obj->pop();
    37. * int param_3 = obj->peek();
    38. * bool param_4 = obj->empty();
    39. */

    155. 最小栈

    155. Min Stack

            设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

    实现 MinStack 类:

            MinStack() 初始化堆栈对象。
            void push(int val) 将元素val推入堆栈。
            void pop() 删除堆栈顶部的元素。
            int top() 获取堆栈顶部的元素。
            int getMin() 获取堆栈中的最小元素。

            我们可以额外建立一个新栈,栈顶表示原栈里所有值的最小值。每当在原栈里插入一个数字时,若该数字小于等于新栈栈顶,则表示这个数字在原栈里是最小值,我们将其同时插入新栈内。每当从原栈里取出一个数字时,若该数字等于新栈栈顶,则表示这个数是原栈里的最小值之一,我们同时取出新栈栈顶的值。
            一个写起来更简单但是时间复杂度略高的方法是,我们每次插入原栈时,都向新栈插入一次原栈里所有值的最小值(新栈栈顶和待插入值中小的那一个);每次从原栈里取出数字时,同样取出新栈的栈顶。这样可以避免判断,但是每次都要插入和取出。我们这里只展示第一种写法。
    1. class MinStack {
    2. stack<int> s, min_s;
    3. public:
    4. MinStack() {
    5. }
    6. void push(int val) {
    7. s.push(val);
    8. if(min_s.empty() || min_s.top() >= val){
    9. min_s.push(val);
    10. }
    11. }
    12. void pop() {
    13. if(!min_s.empty() && min_s.top() == s.top()){
    14. min_s.pop();
    15. }
    16. s.pop();
    17. }
    18. int top() {
    19. return s.top();
    20. }
    21. int getMin() {
    22. return min_s.top();
    23. }
    24. };
    25. /**
    26. * Your MinStack object will be instantiated and called as such:
    27. * MinStack* obj = new MinStack();
    28. * obj->push(val);
    29. * obj->pop();
    30. * int param_3 = obj->top();
    31. * int param_4 = obj->getMin();
    32. */

    20. 有效的括号

    20. Valid Parentheses

            给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

    有效字符串需满足:

            左括号必须用相同类型的右括号闭合。
            左括号必须以正确的顺序闭合。
            每个右括号都有一个对应的相同类型的左括号。

            括号匹配是典型的使用栈来解决的问题。我们从左往右遍历,每当遇到左括号便放入栈内,遇到右括号则判断其和栈顶的括号是否是统一类型,是则从栈内取出左括号,否则说明字符串不合法。
    1. class Solution {
    2. public:
    3. bool isValid(string s) {
    4. stack<int> parsed;
    5. for(int i=0; ilength(); ++i){
    6. if(s[i] == '(' || s[i] == '[' || s[i] == '{'){
    7. parsed.push(s[i]);
    8. }else{
    9. if(parsed.empty()){
    10. return false;
    11. }
    12. char c = parsed.top();
    13. if((s[i] == ')' && c == '(') || (s[i] == ']' && c == '[') || (s[i] == '}' && c == '{')){
    14. parsed.pop();
    15. }else {
    16. return false;
    17. }
    18. }
    19. }
    20. return parsed.empty();
    21. }
    22. };

    3. 单调栈

            单调栈通过维持栈内值的单调递增(递减)性,在整体 O ( n ) 的时间内处理需要大小比较的问题。

    739. 每日温度

    739. Daily Temperatures

            给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

            我们可以维持一个单调递减的栈,表示每天的温度;为了方便计算天数差,我们这里存放位置(即日期)而非温度本身。我们从左向右遍历温度数组,对于每个日期 p ,如果 p 的温度比栈顶存储位置 q 的温度高,则我们取出 q ,并记录 q 需要等待的天数为 p q ;我们重复这一过程,直到 p 的温度小于等于栈顶存储位置的温度(或空栈)时,我们将 p 插入栈顶,然后考虑下一天。在这个过程中,栈内数组永远保持单调递减,避免了使用排序进行比较。最后若栈内剩余一些日期,则说明它们之后都没有出现更暖和的日期。
    1. class Solution {
    2. public:
    3. vector<int> dailyTemperatures(vector<int>& temperatures) {
    4. stack<int> indices;
    5. int n = temperatures.size();
    6. vector<int> ans(n);
    7. for(int i=0; i
    8. while(!indices.empty()){
    9. int pre_index = indices.top();
    10. if(temperatures[i] <= temperatures[pre_index]){
    11. break;
    12. }
    13. indices.pop();
    14. ans[pre_index] = i - pre_index;
    15. }
    16. indices.push(i);
    17. }
    18. return ans;
    19. }
    20. };

    4. 优先队列

            优先队列(priority queue )可以在 O ( 1 ) 时间内获得最大值,并且可以在 O ( log n ) 时间内取出 最大值或插入任意值。
            优先队列常常用(heap )来实现。堆是一个完全二叉树,其每个节点的值总是大于等于子节点的值。实际实现堆时,我们通常用一个数组而不是用指针建立一个树。这是因为堆是完全二叉树,所以用数组表示时,位置 i 的节点的父节点位置一定为 (i-1)/2 ,而它的两个子节点的位置又一定分别为 2i+1 2i+2
            以下是堆的实现方法,其中最核心的两个操作是上浮和下沉:如果一个节点比父节点大,那么需要交换这个两个节点;交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,我们称之为上浮;类似地,如果一个节点比父节小,也需要不断地向下进行比较和交换操作, 我们称之为下沉。如果一个节点有两个子节点,我们总是交换最大的子节点。
    (最大)堆,维护的是数据结构中的大于关系
    1. vector<int> heap;
    2. // 上浮
    3. void swim(int pos){
    4. while(pos > 0 && heap[(pos - 1)/2] < heap[pos]){
    5. swap(heap[(pos - 1)/2], heap[pos]);
    6. pos = (pos - 1)/2;
    7. }
    8. }
    9. // 下沉
    10. void sink(int pos){
    11. while(2 * pos + 1 <= N){
    12. int i = 2 * pos + 1;
    13. if(i < N && heap[i] < heap[i + 1]) ++i;
    14. if(heap[pos] >= heap[i]) break;
    15. swap(heap[pos], heap[i]);
    16. pos = i;
    17. }
    18. }
    19. // 插入任意值:把新的数字放在最后一位,然后上浮
    20. void push(int x){
    21. heap.push_back(x);
    22. swim(heap.size() - 1);
    23. }
    24. // 删除最大值:把最后一个数字挪到开头,然后下沉
    25. void pop(){
    26. heap[0] = heap.back();
    27. heap.pop_back();
    28. sink(0);
    29. }
    30. // 获得最大值
    31. int top(){
    32. return heap[0];
    33. }
            通过将算法中的大于号和小于号互换,我们也可以得到一个快速获得最小值的优先队列。
            另外,正如我们在 STL 章节提到的那样,如果我们需要在维持大小关系的同时,还需要支查找任意值、删除任意值、维护所有数字的大小关系等操作,可以考虑使用 set map 来代替优先队列。

    23. 合并K个升序链表

    23. Merge k Sorted Lists

            给你一个链表数组,每个链表都已经按升序排列。

            请你将所有链表合并到一个升序链表中,返回合并后的链表。

            本题可以有很多中解法,比如类似于归并排序进行两两合并。我们这里展示一个速度比较快的方法,即把所有的链表存储在一个优先队列中, 每次提取所有链表头部节点值最小的那个节点,直到所有链表都被提取完为止。注意因为 Comp 函数默认是对最大堆进行比较并维持递增关系,如果我们想要获取最小的节点值,则我们需要实现一个最小堆,因此比较函数应该维持递减关系,所以 operator() 中返回时用大于号而不是等增关系时的小于号进行比较。
    1. /**
    2. * Definition for singly-linked list.
    3. * struct ListNode {
    4. * int val;
    5. * ListNode *next;
    6. * ListNode() : val(0), next(nullptr) {}
    7. * ListNode(int x) : val(x), next(nullptr) {}
    8. * ListNode(int x, ListNode *next) : val(x), next(next) {}
    9. * };
    10. */
    11. class Solution {
    12. // 自定义优先队列的元素比较方法 Compa
    13. struct Comp{
    14. bool operator() (ListNode* l1, ListNode* l2){
    15. return l1->val > l2->val;
    16. }
    17. };
    18. public:
    19. ListNode* mergeKLists(vector& lists) {
    20. if(lists.empty()) return nullptr;
    21. priority_queue, Comp> q;
    22. for(ListNode* list: lists){
    23. if(list){
    24. q.push(list);
    25. }
    26. }
    27. ListNode* dummy = new ListNode(0), *cur = dummy;
    28. while(!q.empty()){
    29. // 取出最小的节点插入新链表
    30. cur->next = q.top();
    31. q.pop();
    32. cur = cur->next;
    33. // 再将这个节点的后继节点放入队列中
    34. if(cur->next){
    35. q.push(cur->next);
    36. }
    37. }
    38. return dummy->next;
    39. }
    40. };

    218. 天际线问题

    218. The Skyline Problem

            城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回 由这些建筑物形成的 天际线 。

            每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:

            lefti 是第 i 座建筑物左边缘的 x 坐标。
            righti 是第 i 座建筑物右边缘的 x 坐标。
            heighti 是第 i 座建筑物的高度。
            你可以假设所有的建筑都是完美的长方形,在高度为 0 的绝对平坦的表面上。

            天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],...] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。

            注意:输出天际线中不得有连续的相同高度的水平线。例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[...[2 3], [4 5], [12 7], ...]

            我们可以使用优先队列储存每个建筑物的高度和右端(这里使用 pair,其默认比较函数是先比较第一个值,如果相等则再比较第二个值),从而获取目前会拔高天际线、且妨碍到前一个建筑物(的右端端点)的下一个建筑物。

            如果是「从下到上」转向「水平方向」,纵坐标最大的点是关键点,不pop();
            如果是「从上到下」转向「水平方向」,纵坐标第二大的点是关键点,pop()。

    1. class Solution {
    2. public:
    3. vectorint>> getSkyline(vectorint>>& buildings) {
    4. vectorint>> ans;
    5. priority_queueint, int>> max_heap; // <高度, 右端>
    6. int i = 0, len = buildings.size();
    7. int cur_x, cur_h;
    8. // 保证建筑没遍历完,或者遍历完了但是max_heap没遍历完
    9. while(i < len || !max_heap.empty()){
    10. // 如果当前建筑左坐标比优先队列最高建筑的右坐标小,判断上升转折点
    11. if(max_heap.empty() || i < len && buildings[i][0] <= max_heap.top().second){
    12. cur_x = buildings[i][0];
    13. // 所有左端相同的建筑放入优先队列
    14. while(i < len && cur_x == buildings[i][0]){
    15. max_heap.emplace(buildings[i][2], buildings[i][1]);\
    16. ++i;
    17. }
    18. }else{
    19. // 如果当前建筑左坐标比优先队列的最大高度建筑的右坐标大,说明一群建筑结束,可以开始判断该群建筑的所有下降转折点了
    20. cur_x = max_heap.top().second;
    21. // 把所有优先队列内比头右坐标小的剔除(下降点只会在最高点的右边)
    22. while(!max_heap.empty() && cur_x >= max_heap.top().second){
    23. max_heap.pop();
    24. }
    25. }
    26. cur_h = (max_heap.empty())? 0: max_heap.top().first;
    27. // 因为只有判断下降时才会pop优先队列,所以在同一群建筑中和优先队列最高高度不一样才加入输出
    28. if(ans.empty() || cur_h != ans.back()[1]){
    29. ans.push_back({cur_x, cur_h});
    30. }
    31. }
    32. return ans;
    33. }
    34. };

    5. 双端队列

    239. 滑动窗口最大值

    239. Sliding Window Maximum

            给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

            返回 滑动窗口中的最大值 。

            我们可以利用双端队列(单调队列)进行操作:每当向右移动时,把窗口左端的值从队列左端剔除,把队列右边小于窗口右端的值全部剔除。这样双端队列的最左端永远是当前窗口内的最大值。另外,这道题也是单调栈的一种延申:该双端队列利用从左到右递减来维持大小关系。

            双端队列的作用是保证每次L边界右移时从队列头弹出的都是当前窗口的最大值,队列中存储元素下标即可。

    1. class Solution {
    2. public:
    3. vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    4. deque<int> dq;
    5. vector<int> ans;
    6. for(int i=0; isize(); ++i){
    7. // 如果首元素超过滑动窗口,弹出
    8. if(!dq.empty() && dq.front() == i - k){
    9. dq.pop_front();
    10. }
    11. // 构造单调递减队列
    12. while(!dq.empty() && nums[i] > nums[dq.back()]){
    13. dq.pop_back();
    14. }
    15. dq.push_back(i);
    16. // 在滑动窗口内每次都要添加最大值
    17. if(i >= k - 1){
    18. ans.push_back(nums[dq.front()]);
    19. }
    20. }
    21. return ans;
    22. }
    23. };

    6. 哈希表

            哈希表,又称散列表,使用 O(n) 空间复杂度存储数据,通过哈希函数映射位置,从而实现近似 O(1) 时间复杂度的插入、查找、删除等操作。 C++ 中的哈希集合为 unordered_set,可以查找元素是否在集合中。如果需要同时存储键和值,则需要用 unordered_map,可以用来统计频率,记录内容等等。如果元素有穷,并且范围不大,那么可以用一个固定大小的数组来存储或统计元素。例如我们需要统计一个字符串中所有字母的出现次数,则可以用一个长度为 26 的数组来进行统计,其哈希函数即为字母在字母表的位置,这样空间复杂度就可以降低为常数。

            一个简单的哈希表的实现如下。

    1. // 一个简单的对整数实现的哈希函数
    2. int MyHash(const int& key, const int& tableSize){
    3. return key % tableSize;
    4. }
    5. template <typename T>;
    6. class HashTable{
    7. public:
    8. // size最好是质数
    9. HashTable(int size = 31){
    10. hash_table_.reverse(size);
    11. hash_table_.reverse(size);
    12. }
    13. ~HashTable();
    14. // 查找哈希表是否存在该值
    15. bool Contains(const T& obj){
    16. const list& slot = hash_table_[Hash(obj)];
    17. auto it = slot.cbegin();
    18. for(;it != slot.cend() && *it != obj; ++it);
    19. return it != slot.cend();
    20. }
    21. // 插入值
    22. bool Insert(const T& obj){
    23. if(Contains(obj)){
    24. return false;
    25. }
    26. hash_table_[Hash(obj)].push_front(obj);
    27. return true;
    28. }
    29. // 删除值
    30. bool Remove(const T& obj){
    31. list& slot = hash_table_[Hash(obj)];
    32. auto it = find(slot.begin(), slot.end(), obj);
    33. if(it == slot.end()){
    34. return false;
    35. }
    36. slot.erase(it);
    37. return true;
    38. }
    39. private:
    40. vector> hash_table_;
    41. // 哈希函数
    42. int Hash(const T& obj) const{
    43. return MyHash(obj, hash_table_.size());
    44. }
    45. }

            如果需要大小关系的维持,且插入查找并不过于频繁,则可以使用有序的 set/map 来代替 unordered_set/unordered_map。

    1. 两数之和

    1. Two Sum

            给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

            你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

            你可以按任意顺序返回答案。

    1. class Solution {
    2. public:
    3. vector<int> twoSum(vector<int>& nums, int target) {
    4. // 键是数字,值是该数字在数组的位置
    5. unordered_map<int, int> hash;
    6. vector<int> ans;
    7. for(int i=0; isize(); ++i){
    8. int num = nums[i];
    9. auto pos = hash.find(target - num);
    10. if(pos == hash.end()){
    11. hash[num] = i;
    12. }else{
    13. ans.push_back(pos->second);
    14. ans.push_back(i);
    15. break;
    16. }
    17. }
    18. return ans;
    19. }
    20. };

    128. 最长连续序列

    128. Longest Consecutive Sequence

            给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

            请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

            我们可以把所有数字放到一个哈希表,然后不断地从哈希表中任意取一个值,并删除掉其之前之后的所有连续数字,然后更新目前的最长连续序列长度。重复这一过程,我们就可以找到所有的连续数字序列。

    1. class Solution {
    2. public:
    3. int longestConsecutive(vector<int>& nums) {
    4. unordered_set<int> hash;
    5. for(const int& num: nums){
    6. hash.insert(num);
    7. }
    8. int ans = 0;
    9. while(!hash.empty()){
    10. int cur = *(hash.begin());
    11. hash.erase(cur);
    12. int next = cur + 1, pre = cur - 1;
    13. while(hash.count(next)){
    14. hash.erase(next++);
    15. }
    16. while(hash.count(pre)){
    17. hash.erase(pre--);
    18. }
    19. ans = max(ans, next - pre - 1);
    20. }
    21. return ans;
    22. }
    23. };

    149. 直线上最多的点数

    149. Max Points on a Line

            给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。

            对于每个点,我们对其它点建立哈希表,统计同一斜率的点一共有多少个。这里利用的原理 是,一条线可以由一个点和斜率而唯一确定。另外也要考虑斜率不存在和重复坐标的情况。 本题也利用了一个小技巧:在遍历每个点时,对于数组中位置 i 的点,我们只需要考虑 i 之 后的点即可,因为 i 之前的点已经考虑过 i 了。

    1. class Solution {
    2. public:
    3. int maxPoints(vectorint>>& points) {
    4. unordered_map<double, int> hash;
    5. int max_count = 0, same_y = 1, same = 1;
    6. for(int i=0; isize(); ++i){
    7. same_y = 1, same = 1;
    8. for(int j=i+1; jsize(); ++j){
    9. if(points[i][1] == points[j][1]){
    10. ++same_y;
    11. if(points[i][0] == points[j][0]){
    12. ++same;
    13. }
    14. }else{
    15. double dx = points[i][0] - points[j][0];
    16. double dy = points[i][1] - points[j][1];
    17. ++hash[dx/dy];
    18. }
    19. }
    20. max_count = max(max_count, same_y);
    21. for(auto item: hash){
    22. max_count = max(max_count, same + item.second);
    23. }
    24. hash.clear();
    25. }
    26. return max_count;
    27. }
    28. };

    7. 多重集合和映射

    332. 重新安排行程

    332. Reconstruct Itinerary

            给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

            所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

            例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。
            假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

            本题可以先用哈希表记录起止机场,其中键是起始机场,值是一个多重集合,表示对应的终止机场。因为一个人可能坐过重复的线路,所以我们需要使用多重集合储存重复值。储存完成之后,我们可以利用栈来恢复从终点到起点飞行的顺序,再将结果逆序得到从起点到终点的顺序。

    1. class Solution {
    2. public:
    3. vector findItinerary(vector>& tickets) {
    4. vector ans;
    5. if(tickets.empty()){
    6. return ans;
    7. }
    8. unordered_map> hash;
    9. for(const auto& ticket: tickets){
    10. hash[ticket[0]].insert(ticket[1]);
    11. }
    12. stack s;
    13. s.push("JFK");
    14. while(!s.empty()){
    15. string next = s.top();
    16. if(hash[next].empty()){
    17. ans.push_back(next);
    18. s.pop();
    19. }else{
    20. s.push(*hash[next].begin());
    21. hash[next].erase(hash[next].begin());
    22. }
    23. }
    24. reverse(ans.begin(), ans.end());
    25. return ans;
    26. }
    27. };

    8. 前缀和与积分图

            一维的前缀和,二维的积分图,都是把每个位置之前的一维线段或二维矩形预先存储,方便 加速计算。如果需要对前缀和或积分图的值做寻址,则要存在哈希表里;如果要对每个位置记录 前缀和或积分图的值,则可以储存到一维或二维数组里,也常常伴随着动态规划。

    303. 区域和检索 - 数组不可变

    303. Range Sum Query - Immutable

            给定一个整数数组  nums,处理以下类型的多个查询:

            计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right
    实现 NumArray 类:

            NumArray(int[] nums) 使用数组 nums 初始化对象
            int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )

    1. class NumArray {
    2. vector<int> psum;
    3. public:
    4. NumArray(vector<int>& nums): psum(nums.size() + 1, 0){
    5. partial_sum(nums.begin(), nums.end(), psum.begin() + 1);
    6. }
    7. int sumRange(int left, int right) {
    8. return psum[right + 1] - psum[left];
    9. }
    10. };
    11. /**
    12. * Your NumArray object will be instantiated and called as such:
    13. * NumArray* obj = new NumArray(nums);
    14. * int param_1 = obj->sumRange(left,right);
    15. */

    304. 二维区域和检索 - 矩阵不可变

    304. Range Sum Query 2D - Immutable

            给定一个二维矩阵 matrix,以下类型的多个请求:

            计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为(row2, col2) 。
    实现 NumMatrix 类:

            NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化 int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素总和 。

            类似于前缀和,我们可以把这种思想拓展到二维,即积分图(image integral)。我们可以先建 立一个 intergral 矩阵,intergral[i][j] 表示以位置 (0, 0) 为左上角、位置 (i-1, j-1) 为右下角的长方形 中所有数字的和。 如图 1 所示,我们可以用动态规划来计算 integral 矩阵:intergral[i][j] = matrix[i-1][j-1] + integral[i-1][j] + integral[i][j-1] - integral[i-1][j-1],即当前坐标的数字 + 上面长方形的数字和 + 左 边长方形的数字和 - 上面长方形和左边长方形重合面积(即左上一格的长方形)中的数字和。

    图 1 - 左边为给定矩阵,右边为积分图结果,右下角位置的积分图值为 5+48+45−40 = 58

             如图 2 所示,假设我们要查询长方形 E 的数字和,因为 E = D − B − C + A,我们发现 E 其 实可以由四个位置的积分图结果进行加减运算得到。因此这个算法在预处理时的时间复杂度为 O(mn),而在查询时的时间复杂度仅为 O(1)。

    图 2 - 左边为给定矩阵,右边为积分图结果,长方形 E 的数字和等于 58 − 11 − 13 + 3 = 37

     

    1. class NumMatrix {
    2. vectorint>> integral;
    3. public:
    4. NumMatrix(vectorint>>& matrix) {
    5. int m = matrix.size(), n = m > 0? matrix[0].size(): 0;
    6. integral = vectorint>>(m + 1, vector<int>(n + 1, 0));
    7. for(int i=1; i<=m; ++i){
    8. for(int j=1; j<=n; ++j){
    9. integral[i][j] = matrix[i - 1][j - 1] + integral[i - 1][j] + integral[i][j - 1] - integral[i - 1][j - 1];
    10. }
    11. }
    12. }
    13. int sumRegion(int row1, int col1, int row2, int col2) {
    14. return integral[row2 + 1][col2 + 1] - integral[row1][col2 + 1] - integral[row2 + 1][col1] + integral[row1][col1];
    15. }
    16. };
    17. /**
    18. * Your NumMatrix object will be instantiated and called as such:
    19. * NumMatrix* obj = new NumMatrix(matrix);
    20. * int param_1 = obj->sumRegion(row1,col1,row2,col2);
    21. */

    560. 和为 K 的子数组

    560. Subarray Sum Equals K

            给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数 

            本题同样是利用前缀和,不同的是这里我们使用一个哈希表 hashmap,其键是前缀和,而值是 该前缀和出现的次数。在我们遍历到位置 i 时,假设当前的前缀和是 psum,那么 hashmap[psum-k] 即为以当前位置结尾、满足条件的区间个数。

    1. class Solution {
    2. public:
    3. int subarraySum(vector<int>& nums, int k) {
    4. int count = 0, psum = 0;
    5. unordered_map<int, int> hashmap;
    6. hashmap[0] = 1; // 初始化很重要
    7. for(int i: nums){
    8. psum += i;
    9. count += hashmap[psum - k];
    10. ++hashmap[psum];
    11. }
    12. return count;
    13. }
    14. };

    三、巩固练习


    欢迎大家共同学习和纠正指教

  • 相关阅读:
    软件测试进阶(黑白盒测试)
    python用PyPDF2函数库方法对pdf文件切割
    【MySQL】基本查询(表的增删改查)-- 详解
    《DevOps 精要:业务视角》- 读书笔记(七)
    python在线及离线安装库
    C语言基础语法复习06-c语言关键字
    NFT 为何能够重塑艺术价值?
    SpringBoot 学习(一)自动装配
    电脑重装系统后如何删除微软商店下载记录
    Marked.js让您的文档编辑更加轻松自如!
  • 原文地址:https://blog.csdn.net/Hello_world_n/article/details/127602085