leetcode-11 数据结构

11.1 C++ STL

  • 序列容器:维持顺序的容器
    • vector动态数组,随机读取的时间复杂度为O(1),尾部增删的时间复杂度为O(1)
    • list双向链表(可以当做stack和queue)
    • deque双端队列,既支持O(1)随机读取,又支持O(1)头部增删和尾部增删
    • array固定大小的数组
    • forward_list单向链表
  • 容器适配器:基于其它容器实现的数据结构
    • stack后入先出,默认基于deque实现。stack常用于深度优先搜索字符串匹配问题单调栈问题
    • queue先入先出,默认基于deque实现。queue常用于广度优先搜索
    • priority_queue最大值先出,默认基于vector实现堆结构。支持O(nlogn)排序数组,O(logn)插入任意值、删除最大值,O(1)获取最大值
  • 关联容器:实现了排好序的数据结构
    • set有序集合,元素不可重复,底层实现默认为红黑树(一种特殊的二叉查找树BST)。支持O(nlogn)排序数组,O(logn)插入、删除、查找、获取最大/最小值
    • multiset:支持重复元素的set
    • map有序映射或有序表,在set的基础上加上映射关系,可以对每个元素key存一个值value
    • multimap:支持重复元素的map
  • 未排序的关联容器:对每个关联容器实现了哈希版本
    • unordered_set哈希集合,支持O(1)插入、删除、查找元素,常用于快速查询一个元素是否在这个容器内
    • unordered_multiset:支持重复元素的unordered_set
    • unordered_map哈希映射或哈希表,在unordered_set的基础上加上映射关系,可以对每一个元素key存一个值value
    • unordered_multimap:支持重复元素的unordered_map

11.2 数组

  • 448 找到所有数组中消失的数字

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1]
输出:[5,6]
示例 2:
输入:nums = [1,1]
输出:[2]

// 方法1
// 1.使用一个数组的下标记录每个对应数字出现的次数
// 2.遍历数组,根据值为0的元素所在的下标确定没有出现过的数字
std::vector<int> findDisappearedNumbers(std::vector<int> &nums) {
    std::vector<int> count(nums.size());
    std::vector<int> result;
    for (int i = 0; i < nums.size(); ++i) {
        count[nums[i] - 1]++;
    }
    for (int i = 0; i < nums.size(); ++i) {
        if (count[i] == 0) {
            result.push_back(i + 1);
        }
    }
    return result;
}
//方法2
//直接对原数组进行标记:将下标为(出现的数字-1)的元素的值设为负数,最后值为正数的元素所在下标+1即为没有出现过的数字
std::vector<int> findDisappearedNumbers(std::vector<int> &nums) {
    std::vector<int> result;
    for (int i = 0; i < nums.size(); ++i) {
        int position = std::abs(nums[i]) - 1;
        if (nums[position] > 0) {
            nums[position] = -nums[position];
        }
    }
    for (int i = 0; i < nums.size(); ++i) {
        if (nums[i] > 0) {
            result.push_back(i + 1);
        }
    }
    return result;
}
  • 48 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

// 方法1
//(i,j)位置的元素旋转90°后变成了(j,n-1-i)位置(n为二维数组大小)
void rotate(vector<vector<int>> &matrix) {
    auto matrix_new = matrix;
    int n = matrix.size();
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            matrix_new[j][n - 1 - i] = matrix[i][j];
        }
    }
    matrix = matrix_new;
}
// 方法2
// 每4个元素为一组:matrix[i][j] => matrix[j][n-1-i] => matrix[n-1-i][n-1-j] => matrix[n-1-j][i]
// 如何确定遍历的范围:如果n为偶数,则需要遍历n^2/4=(n/2)*(n/2)个元素;如果n为奇数,则需要遍历(n^2-1)/4=(n-1)/2*(n+1)/2个元素
void rotate(vector<vector<int>>& matrix) {
    int n = matrix.size();
    int temp;
    for (int i = 0; i < n / 2; ++i) {
        for (int j = 0; j < (n + 1) / 2; ++j) {
            temp = matrix[n - 1 - j][i];
            matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
            matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
            matrix[j][n - 1 - i] = matrix[i][j];
            matrix[i][j] = temp;
        }
    }
}
  • 240 搜索二维矩阵II

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
示例1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true

// 关键技巧:从右上角开始搜索,如果当前值大于目标值,则往左移动;如果当前值小于目标值,则往下移动
bool searchMatrix(vector<vector<int>> &matrix, int target) {
    int m = matrix.size();
    int n = matrix[0].size();
    int i{0}, j{n - 1};
    while (i < m && j >= 0) {
        if (matrix[i][j] < target) {
            i++;
        } else if (matrix[i][j] > target) {
            j--;
        } else {
            return true;
        }
    }
    return false;
}
  • 769 最多能完成排序的块

给定一个长度为 n 的整数数组 arr ,它表示在 [0, n - 1] 范围内的整数的排列。
我们将 arr 分割成若干 块 (即分区),并对每个块单独排序。将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。
返回数组能分成的最多块数量。
示例 1:
输入: arr = [4,3,2,1,0]
输出: 1
解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。
示例 2:
输入: arr = [1,0,2,3,4]
输出: 4
解释:
我们可以把它分成两块,例如 [1, 0], [2, 3, 4]。
然而,分成 [1, 0], [2], [3], [4] 可以得到最多的块数。
对每个块单独排序后,结果为 [0, 1], [2], [3], [4]

// 从左向右遍历,若遍历过的数中的最大值与当前下标相等,则说明这些遍历过的数可以分割成一个块
int maxChunksToSorted(vector<int> &arr) {
    int result{}, maxVal{-1};
    for (int i = 0; i < arr.size(); ++i) {
        maxVal = max(arr[i], maxVal);
        if (maxVal == i) {
            result++;
        }
    }
    return result;
}
  • 768 最多能完成排序的块II

给你一个整数数组 arr 。
将 arr 分割成若干 块 ,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。
返回能将数组分成的最多块数?
示例 1:
输入:arr = [5,4,3,2,1]
输出:1
解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [5, 4], [3, 2, 1] 的结果是 [4, 5, 1, 2, 3],这不是有序的数组。
示例 2:
输入:arr = [2,1,3,4,4]
输出:4
解释:
可以把它分成两块,例如 [2, 1], [3, 4, 4]。
然而,分成 [2, 1], [3], [4], [4] 可以得到最多的块数。

int maxChunksToSorted(vector<int> &arr) {
    stack<int> maxStack;// 保存每个分段的最大值
    for (int i = 0; i < arr.size(); ++i) {
        if (maxStack.empty() || arr[i] >= maxStack.top()) {
            maxStack.push(arr[i]);
        } else {
            int maxValue = maxStack.top();// 保存当前分段的最大值
            maxStack.pop();
            // 如果之前的分段的最大值大于arr[i],则之前的分段需要和当前的分段合并(为了确保当前分段的每个值都大于等于之前分段的最大值)
            while (!maxStack.empty() && maxStack.top() > arr[i]) {
                maxStack.pop();
            }
            maxStack.push(maxValue);
        }
    }
    return maxStack.size();
}

11.3 栈和队列

  • 232 用栈实现队列
class MyQueue {
// 将一个栈当作输入栈,用于压入 push 传入的数据;另一个栈当作输出栈,用于 pop 和 peek 操作。
// 关键点:只有当输出栈为空时才将输入栈的全部数据依次弹出并压入输出栈
private:
    stack<int> inStack, outStack;

    void in2out() {
        while (!inStack.empty()) {
            outStack.push(inStack.top());
            inStack.pop();
        }
    }

public:

    MyQueue() {

    }

    void push(int x) {
        inStack.push(x);
    }

    int pop() {
        if (outStack.empty()) {
            in2out();
        }
        int res = outStack.top();
        outStack.pop();
        return res;
    }

    int peek() {
        if (outStack.empty()) {
            in2out();
        }
        return outStack.top();
    }

    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};
  • 155 最小栈

设计一个最小栈,除了需要支持常规栈的操作外,还需要支持在 O(1) 时间内查询栈内最小值的功能

// 解法1:使用辅助栈,保存元素栈每次入栈一个元素后的最小值
class MinStack {
private:
    // valueStack用于真正存储元素的值
    // minStack用于存储 valueStack每入栈一个元素后,所有元素中的最小值
    stack<int> valueStack, minStack;
public:
    MinStack() { minStack.push(INT_MAX); }
    void push(int val) {
        valueStack.push(val);
        minStack.push(min(minStack.top(), val));
    }
    void pop() {
        valueStack.pop();
        minStack.pop();
    }
    int top() { return valueStack.top(); }
    int getMin() { return minStack.top(); }
};
// 解法2:栈中同时保存值及其对应时刻的最小值(和解法1的本质是一样的:保存了每个元素入栈时刻的最小值)
class MinStack {
private:
    // 每次元素及其入栈后的那个时刻的最小值
    stack<pair<int, int>> myStack;
public:
    MinStack() {}
    void push(int val) {
        if (myStack.empty()) {
            myStack.push(make_pair(val, val));
        } else {
            myStack.push(make_pair(val, min(myStack.top().second, val)));
        }
    }
    void pop() { myStack.pop(); }
    int top() { return myStack.top().first; }
    int getMin() { return myStack.top().second; }
};
// 解法3:无需保存每个元素入栈时刻的最小值;只有当最小值发生改变时才将其保存下来
class MinStack {
private:
    // valueStack用于真正存储元素的值
    // minStack只保存使得最小值发生变化的那些值,例如入栈valueStack元素为2,3,4,1,0
    // 则只有2,1,0会入栈minStack
    // 也就是说minStack栈底到栈顶保存的是一个非严格的单调递减序列
    stack<int> valueStack, minStack;
public:
    MinStack() {}
    void push(int val) {
        valueStack.push(val);
        // 只有当val是当前最小值时才入栈minStack
        if (minStack.empty() || val <= minStack.top()) {
            minStack.push(val);
        }
    }
    void pop() {
        // 当出栈的元素是当前的最小值时, minStack也需要出栈
        if (!minStack.empty() && minStack.top() == valueStack.top()) {
            minStack.pop();
        }
        valueStack.pop();
    }
    int top() { return valueStack.top(); }
    int getMin() { return minStack.top(); }
};
  • 20 有效的括号

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

// 每当遇到左括号便放入栈内,遇到右括号则判断其和栈顶的括号是否是统一类型,是则从栈内取出左括号,否则说明字符串不合法
bool isValid(string s) {
    stack<char> chStack;
    for (char c: s) {
        if (c == '{' || c == '[' || c == '(') {
            chStack.push(c);
        } else {
            if (chStack.empty())return false;
            char top = chStack.top();
            if (c == '}' && top == '{'
                || c == ']' && top == '['
                || c == ')' && top == '(') {
                chStack.pop();
            } else {
                return false;
            }
        }
    }
    return chStack.empty();
}

11.4 单调栈

  • 739 每日温度

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

// 解法1:暴力法
// 遍历temperatures,对于每个temperaturs[i],找到离它最近并且大于它的temperatures[j]
// 当输入的temperatures数组特别大时,会超时!!
vector<int> dailyTemperatures(vector<int> &temperatures) {
    int n = temperatures.size();
    vector<int> answer(n, 0);
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            if (temperatures[j] > temperatures[i]) {
                answer[i] = j - i;
                break;
            }
        }
    }
    return answer;
}
// 解法2:反向遍历temperatures[],并使用数组firstIndex[]保存每个温度第一次出现时的下标
// 关键点:反向遍历temperatures!!
vector<int> dailyTemperatures(vector<int> &temperatures) {
    int n = temperatures.size();
    vector<int> answer(n, 0);
    vector<int> firstIndex(101, INT_MAX);
    for (int i = n - 1; i >= 0; --i) {
        int warmerIndex = INT_MAX;
        for (int j = temperatures[i] + 1; j <= 100; ++j) {
            warmerIndex = min(warmerIndex, firstIndex[j]);
        }
        if (warmerIndex != INT_MAX) {
            answer[i] = warmerIndex - i;
        }
        firstIndex[temperatures[i]] = i;
    }
    return answer;
}
// 解法3:单调栈
// 遍历temperatures,如果元素的值递减则将其下标入栈,
// 如果temperatures[i]大于栈顶下标对应的值,则将那些小于temperatures[i]的元素对应的下标出栈,同时记录i与出栈的下标之间的差值
vector<int> dailyTemperatures(vector<int> &temperatures) {
    int n = temperatures.size();
    vector<int> answer(n, 0);
    stack<int> indexStack;
    for (int i = 0; i < n; ++i) {
        while (!indexStack.empty() && temperatures[i] > temperatures[indexStack.top()]) {
            answer[indexStack.top()] = i - indexStack.top();
            indexStack.pop();
        }
        indexStack.push(i);
    }
    return answer;
}

11.5 优先队列

  • 最大堆实现

核心操作是上浮(siftUp)、下沉(siftDown)和堆化(heapify)

class MaxHeap {
public:
    // 将一个数组构造成一个最大堆:从最后一个非叶子节点开始依次下沉
    MaxHeap(vector<int> &rawData) : data(rawData) {
        for (int i = parentIndex(data.size() - 1); i >= 0; --i) {
            siftDown(i);
        }
    }

    int size() {
        return data.size();
    }

    bool isEmpty() {
        return data.empty();
    }

    void add(int element) {
        data.push_back(element);
        siftUp(data.size() - 1);
    }

    int pop() {
        int maxElement = data[0];
        data[0] = data[data.size() - 1];
        data.pop_back();
        siftDown(0);
        return maxElement;
    }

    int top() {
        return data[0];
    }

private:
    vector<int> data;

    int parentIndex(int childIndex) {
        return (childIndex - 1) / 2;
    }

    int leftChildIndex(int parentIndex) {
        return parentIndex * 2 + 1;
    }

    int rightChildIndex(int parentIndex) {
        return parentIndex * 2 + 2;
    }

    void siftUp(int position) {
        while (position > 0 && data[position] > data[parentIndex(position)]) {
            swap(data[position], data[parentIndex(position)]);
            position = parentIndex(position);
        }
    }

    void siftDown(int position) {
        while (leftChildIndex(position) < data.size()) {
            int i= leftChildIndex(position);
            int j= rightChildIndex(position);
            int biggerIdx = i;
            if (j< data.size() && data[j] > data[i]) {
                biggerIdx = j;
            }
            if (data[position] >= data[biggerIdx]) {
                break;
            }
            swap(data[position], data[biggerIdx]);
            position = biggerIdx;
        }
    }
};
  • 23 合并K个升序链表
// 单链表定义
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) {}
};
// 自定义优先队列比较规则(根据节点的值构建最小堆)
struct MyCompare {
    bool operator()(ListNode *node1, ListNode *node2) {
        return node1->val > node2->val;
    }
};
// 解法1:将所有链表的所有结点加入优先队列,然后依次取出最小值
ListNode *mergeKLists(vector<ListNode *> &lists) {
    priority_queue<ListNode *, vector<ListNode *>, MyCompare> priorityQueue;
    for (ListNode *listNode: lists) {
        while (listNode) {
            priorityQueue.push(listNode);
            listNode = listNode->next;
        }
    }
    ListNode *first, *last;
    if (priorityQueue.empty()) {
        return nullptr;
    } else {
        first = last = priorityQueue.top();
        priorityQueue.pop();
    }
    while (!priorityQueue.empty()) {
        last->next = priorityQueue.top();
        priorityQueue.pop();
        last = last->next;
    }
    // 避免出现循环
    last->next = nullptr;
    return first;
}
// 解法2:先将所有链表的头结点加入优先队列,然后在取出最小值的过程中继续将其他节点入队
ListNode *mergeKLists(vector<ListNode *> &lists) {
    priority_queue<ListNode *, vector<ListNode *>, MyCompare> priorityQueue;
    for (ListNode *listNode: lists) {
        if (listNode) {
            priorityQueue.push(listNode);
        }
    }
    if (priorityQueue.empty())return nullptr;
    // dummy在第一次指向最小节点后就不会变了
    ListNode *dummy = new ListNode(0), *current = dummy;
    while (!priorityQueue.empty()) {
        current->next = priorityQueue.top();
        priorityQueue.pop();
        current = current->next;
        if (current->next) {
            priorityQueue.push(current->next);
        }
    }
    return dummy->next;
}
  • 218 天际线问题

题意:
简而言之,就是一个平地上有若干长方形建筑,给出了所有长方形建筑的左坐标、右坐标、高度,建筑之间可以覆盖。求返回所有建筑形成的轮廓。用坐标表示,且相同高度不能重复。

/*
  思路:  
  1) 将所有坐标进行排序,依次遍历  
  2) 针对每个坐标,找到覆盖这个坐标的所有长方形,并通过优先队列这种数据结构获取这些长方形的最大高度  
  3) 如果这个高度与前一个坐标对应的最大高度不相同,则说明找到了一个关键点;否则不算一个关键点  
  关键点:  
  1) 寻找覆盖了某个坐标的长方形时,该坐标可以是长方形的左坐标,但不能是长方形的右坐标!因为关键点是`水平线段的左端点`(即:如果一个长方形的左右坐标为(1,2),则这个长方形包含了坐标1但是不包含坐标2)  
  2) 如果没有覆盖该坐标的长方形,说明该坐标的右边是平地,即对应的最大高度为0
*/
vector<vector<int>> getSkyline(vector<vector<int>> &buildings) {
    vector<vector<int>> answer;
    // 1.将所有长方形的左右坐标进行排序
    set<int> coordinates;
    for (vector<int> building: buildings) {
        coordinates.insert(building[0]);
        coordinates.insert(building[1]);
    }

    priority_queue<pair<int, int>> heightAndRight; // 保存长方形的高度和右坐标
    int i = 0;
    for (int coordinate: coordinates) {
        // 2.找到覆盖coordinate这个坐标的所有长方形,并将它们的{高度,右坐标}入队
        // 长方形的左坐标<=coordinate
        // 这里使用了i来避免重复遍历: 因为长方形的左坐标是递增的, 已经遍历过的长方形不用再被遍历, 也就是说长方形最多只入队一次, 不会重复入队
        // 如果每次都使用for循环完整遍历所有长方形的话会超时...
        while (i < buildings.size() && buildings[i][0] <= coordinate) {
            heightAndRight.emplace(buildings[i][2], buildings[i][1]);
            i++;
        }
        // 而长方形的右坐标必须大于coordinate才行
        while (!heightAndRight.empty() && heightAndRight.top().second <= coordinate) {
            heightAndRight.pop();
        }
        // 不能直接在上面的if中直接判断 building[0] <= coordinate && building[1] > coordinate
        // 因为这样的话, 一开始满足条件的长方形会一直存在优先队列中参与运算
        // 例如:当coordinate=2时,长方形(1,3)满足条件入队了, 但是在coordinate=3时, 长方形(1,3)仍然在优先队列中参与比较

        if (heightAndRight.empty()) {
            // 如果没有覆盖该坐标的长方形,说明该坐标的右边是平地,即对应的最大高度为0
            answer.push_back({coordinate, 0});
        } else {
            // 如果这个高度与前一个坐标对应的最大高度不相同,则说明找到了一个关键点;否则不算一个关键点
            if (answer.empty() || (heightAndRight.top().first != answer.back()[1])) {
                answer.push_back({coordinate, heightAndRight.top().first});
            }
        }
    }
    return answer;
}

11.6 双端队列

  • 239 滑动窗口最大值

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

// 解法1:每次构造一个长度为k的vector,并使用std::max_element找到vector中的最大值
// 数据量太大时会超时
vector<int> maxSlidingWindow(vector<int> &nums, int k) {
    vector<int> result;
    for (int i = 0; i <= nums.size() - k; ++i) {
        vector<int> my;
        for (int j = i; j < i + k; ++j) {
            my.push_back(nums[j]);
        }
        result.push_back(*max_element(my.begin(), my.end()));
    }
    return result;
}
// 解法2:使用deque保存k个值,每次只需要出队一个元素、进队一个元素,无需每次构造k个元素的vector
// 数据量太大时还是会超时
vector<int> maxSlidingWindow(vector<int> &nums, int k) {
    vector<int> result;
    deque<int> myDeque;
    for (int i = 0; i < k; ++i) {
        myDeque.push_back(nums[i]);
    }
    result.push_back(*max_element(myDeque.begin(), myDeque.end()));
    for (int j = k; j < nums.size(); ++j) {
        myDeque.push_back(nums[j]);
        myDeque.pop_front();
        result.push_back(*max_element(myDeque.begin(), myDeque.end()));
    }
    return result;
}
// 解法3:优先队列
// 初始时,将数组nums的前k个元素放入优先队列中。
// 每次向右移动窗口时,把一个新的元素放入优先队列中,此时堆顶的元素就是队列中所有元素的最大值
// 但是这个最大值可能并不在当前窗口中,因此将其从优先队列中剔除
// 直到堆顶元素位于当前窗口中
vector<int> maxSlidingWindow(vector<int> &nums, int k) {
    vector<int> result;
    priority_queue<pair<int, int>> priorityQueue; // 保存元素的值及其下标
    for (int i = 0; i < k; ++i) {
        priorityQueue.emplace(nums[i], i);
    }
    result.push_back(priorityQueue.top().first);
    for (int i = k; i < nums.size(); ++i) {
        priorityQueue.emplace(nums[i], i);
        // 如果优先队列中的最大值不在窗口中则剔除
        // 当前窗口的元素下标范围为[i-k+1,i]
        while (priorityQueue.top().second < i - k + 1) {
            priorityQueue.pop();
        }
        result.push_back(priorityQueue.top().first);
    }
    return result;
}
// 解法4:单调队列
// 使用deque保存元素下标,这些下标对应的元素保持严格单调递减,因此队首下标对应的元素是所有元素的最大值
// 因此当前窗口中的最大值就是队首下标对应的元素,和解法3一样,前提是队首下标在当前窗口的下标范围内,否则将该下标出队
vector<int> maxSlidingWindow(vector<int> &nums, int k) {
    vector<int> result;
    deque<int> myDeque; // 保存元素的下标, 这些下标对应的元素是严格单调递减的
    for (int i = 0; i < k; ++i) {
        while (!myDeque.empty() && nums[myDeque.back()] <= nums[i]) {
            myDeque.pop_back();
        }
        myDeque.push_back(i);
    }
    result.push_back(nums[myDeque.front()]);
    for (int i = k; i < nums.size(); ++i) {
        while (!myDeque.empty() && nums[myDeque.back()] <= nums[i]) {
            myDeque.pop_back();
        }
        myDeque.push_back(i);
        // 当前窗口的下标范围是[i-k+1,i]
        // 如果队首元素在当前窗口左侧, 则将其从队列中剔除
        while (myDeque.front() < i - k + 1) {
            myDeque.pop_front();
        }
        result.push_back(nums[myDeque.front()]);
    }
    return result;
}

11.7 哈希表

  • 1 两数之和

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

// 解法1:暴力解法
vector<int> twoSum(vector<int> &nums, int target) {
    for (int i = 0; i < nums.size(); ++i) {
        for (int j = i + 1; j < nums.size(); ++j) {
            if ((nums[i] + nums[j]) == target) {
                return {i, j};
            }
        }
    }
    return {};
}
// 解法2:使用哈希表存储元素的值及其下标,每次遍历数组元素nums[i]时,都在哈希表里查找是否存在key为target-nums[i]的元素
vector<int> twoSum(vector<int> &nums, int target) {
    unordered_map<int, int> value2Index;
    for (int i = 0; i < nums.size(); ++i) {
        if (value2Index.find(target - nums[i]) != value2Index.end()) {
            return {i, value2Index[target - nums[i]]};
        } else {
            value2Index.insert({nums[i], i});
        }
    }
    return {};
}
  • 128 最长连续序列

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

// 解法1:暴力求解,数据量太大时超时
int longestConsecutive(vector<int> &nums) {
    sort(nums.begin(), nums.end());
    int result = 0;
    for (int i = 0; i < nums.size(); ++i) {
        int length = 1;
        int current = nums[i];
        for (int j = i + 1; j < nums.size(); ++j) {
            if (nums[j] == current) {
                continue;
            }
            if (nums[j] == current + 1) {
                length++;
                current++;
            } else {
                i = j - 1;
                break;
            }
        }
        result = max(result, length);
    }
    return result;
}
// 解法2:把所有数字放到一个哈希表,然后不断地从哈希表中任意取一个值,并删除掉其之前之后的所有连续数字,然后更新目前的最长连续序列长度。
// 因为哈希表的增删查的时间复杂度为O(1)
// 通过删除已经遍历过的数字来避免重复遍历!!
int longestConsecutive(vector<int> &nums) {
    unordered_set<int> set;
    for (int num: nums) {
        set.insert(num);
    }
    int ans = 0;
    while (!set.empty()) {
        int current = *set.begin();
        set.erase(current);
        int next = current + 1, prev = current - 1;
        while (set.count(next)) {
            set.erase(next++);
        }
        while (set.count(prev)) {
            set.erase(prev--);
        }
        ans = max(ans, next - prev - 1);
    }
    return ans;
}
// 解法3:把所有数字放到一个哈希表,然后进行遍历。遍历到元素x时,则判断x+1,x+2...是否存在
// 如果x-1存在,则跳过x,因为遍历到x-1时,会继续判断x是否存在,这样也能避免重复遍历
int longestConsecutive(vector<int> &nums) {
    unordered_set<int> set;
    int ans = 0;
    for (int num: nums) {
        set.insert(num);
    }
    for (int num: set) {
        if (set.count(num - 1))continue;
        int currentConsecutive = 1;
        while (set.count(num + 1)) {
            currentConsecutive++;
            num++;
        }
        ans = max(ans, currentConsecutive);
    }
    return ans;
}
  • 149 直线上最多的点

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

// 解法1:暴力解法,使用乘法来避免除法的浮点误差
// 但是如果乘积太大可能会溢出,这时候可以将乘积的结果用long来保存
// 如果存在相同的点,则需要加上被注释的两条continue语句
int maxPoints(vector<vector<int>> &points) {
    int ans = 1;
    for (int i = 0; i < points.size(); ++i) {
        for (int j = i + 1; j < points.size(); ++j) {
            int x1 = points[i][0], y1 = points[i][1];
            int x2 = points[j][0], y2 = points[j][1];
            // if (x1 == x2 && y1 == y2)continue;
            int pointsOnLine = 2;
            int dx = x2 - x1, dy = y2 - y1;
            for (int k = j + 1; k < points.size(); ++k) {
                // if ((points[k][0] == x1 & points[k][1] == y1) || (points[k][0] == x2 & points[k][1] == y2)) continue;
                if (dy * (points[k][0] - x1) == dx * (points[k][1] - y1)) {
                    pointsOnLine++;
                }
            }
            ans = max(ans, pointsOnLine);
        }
    }
    return ans;
}
// 解法2:使用string来存储斜率的分子和分母,避免使用浮点数
int maxPoints(vector<vector<int>> &points) {
    int ans = 1;
    for (int i = 0; i < points.size(); ++i) {
        unordered_map<string, int> slope2Count; // 记录每个斜率(需要转化为最简分数)及其对应的点数
        int x1 = points[i][0], y1 = points[i][1];
        for (int j = i + 1; j < points.size(); ++j) {
            int x2 = points[j][0], y2 = points[j][1];
            // if (x1 == x2 && y1 == y2)continue; // 如果存在相同点需要跳过
            int g = gcd(y2 - y1, x2 - x1);
            string key = to_string((y2 - y1) / g) + "_" + to_string((x2 - x1) / g);
            slope2Count[key]++;
            ans= max(ans, slope2Count[key] + 1); // 1指的是points[i]自己
        }
    }
    return ans;
}
// 辗转相除法求最大公约数
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}
// 解法3:直接使用double保存斜率
int maxPoints(vector<vector<int>> &points) {
    int ans = 1;
    for (int i = 0; i < points.size(); ++i) {
        unordered_map<double, int> slope2Count; // 记录每个斜率及其对应的点数
        int x1 = points[i][0], y1 = points[i][1];
        int verticalCount = 1; // 垂直于x轴的点数
        for (int j = i + 1; j < points.size(); ++j) {
            int x2 = points[j][0], y2 = points[j][1];
            // if (x1 == x2 && y1 == y2)continue; // 如果存在相同点需要跳过
            if (x1 == x2) {
                verticalCount++;
                continue;
            }
            double slope = (double) (y2 - y1) / (x2 - x1);
            slope2Count[slope]++;
            ans = max(ans, slope2Count[slope] + 1); // 1指的是points[i]自己
        }
        ans = max(ans, verticalCount);
    }
    return ans;
}

11.8 多重集合和映射

  • 332 重新安排行程

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

// 错误解法:当输入为[["JFK","KUL"],["JFK","NRT"],["NRT","JFK"]]时,实际输出是["JFK","KUL"],预期输出是["JFK","NRT","JFK","KUL"]
// 这是因为,从起点往终点遍历时,遍历到"KUL"后无法继续遍历,跳出循环
vector<string> findItinerary(vector<vector<string>> &tickets) {
    vector<string> ans = {"JFK"};
    // 每个起点对应多个终点; 并且multiset的元素默认排序
    unordered_map<string, multiset<string>> start2Destinations;
    for (const vector<string> &ticket: tickets) {
        start2Destinations[ticket[0]].insert(ticket[1]);
    }
    string findStr = "JFK";
    while (!start2Destinations.empty()) {
        multiset<string> &destinations = start2Destinations[findStr]; // 注意:这个必须使用引用,否则删除不会影响原容器
        if (destinations.empty())
            break;
        auto i = *(destinations.begin());
        ans.push_back(i);
        destinations.erase(destinations.begin());
        if (destinations.empty()) {
            start2Destinations.erase(findStr);
        }
        findStr = i;
    }
    return ans;
}
// 由于不能从起点往终点遍历,因此使用栈来保存行程轨迹,然后逆序输出
vector<string> findItinerary(vector<vector<string>> &tickets) {
    vector<string> ans = {};
    // 每个起点对应多个终点; 并且multiset的元素默认排序
    unordered_map<string, multiset<string >> start2Destinations;
    for (const vector<string> &ticket: tickets) {
        start2Destinations[ticket[0]].insert(ticket[1]);
    }
    stack<string> track;
    track.emplace("JFK");
    while (!track.empty()) {
        string top = track.top();
        multiset<string> &destinations = start2Destinations[top]; // 注意:这个必须使用引用,否则删除不会影响原容器
        if (!destinations.empty()) {
            track.push(*destinations.begin());
            destinations.erase(destinations.begin());
        } else {
            ans.push_back(top);
            track.pop();
        }
    }
    reverse(ans.begin(), ans.end()); // 翻转后才是起点到终点
    return ans;
}

11.9 前缀和与积分图

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

给定一个整数数组 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 {
public:
    NumArray(vector<int> &nums) {
        int sum = 0;
        for (int i = 0; i < nums.size(); ++i) {
            sum += nums[i];
            prefixSum.push_back(sum);
        }
    }

    int sumRange(int left, int right) {
        if (left <= 0) {
            return prefixSum[right];
        } else {
            return prefixSum[right] - prefixSum[left - 1];
        }
    }

private:
    vector<int> prefixSum{}; // prefixSum = nums[0]+..+nums[i]
};
// 解法2
class NumArray {
private:
    vector<int> prefixSum;
public:
    NumArray(vector<int> &nums) {
        prefixSum = vector<int>(nums.size() + 1, 0);
        for (int i = 0; i < nums.size(); ++i) {
            prefixSum[i + 1] = prefixSum[i] + nums[i];
        }
    }

    int sumRange(int left, int right) {
        return prefixSum[right + 1] - prefixSum[left];
    }
};
  • 304 二维区域和检索-矩阵不可变

给定一个二维矩阵 matrix,以下类型的多个请求:
计算其子矩形范围内元素的总和,该子矩阵的 左上角 为 (row1, col1) ,右下角 为 (row2, col2) 。
实现 NumMatrix 类:
NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素 总和 。

// 解法1
class NumMatrix {
public:
    NumMatrix(vector<vector<int>> &matrix) {
        // 如果matrix是mxn, 则accumulationMatrix就是(m+1)x(n+1)
        // accumulationMatrix[i][j]就是以matrix[0][0]和matrix[i-1][j-1]为对角线的矩形的元素和
        accumulationMatrix.resize(matrix.size() + 1);
        for (vector<int> &vec: accumulationMatrix) { // 注意!这里必须使用引用类型
            vec.resize(matrix[0].size() + 1);
        }
        for (int i = 1; i <= matrix.size(); ++i) {
            for (int j = 1; j <= matrix[0].size(); ++j) {
                accumulationMatrix[i][j] =
                        accumulationMatrix[i - 1][j] + accumulationMatrix[i][j - 1] - accumulationMatrix[i - 1][j - 1] +
                        matrix[i - 1][j - 1];
            }
        }
    }

    int sumRegion(int row1, int col1, int row2, int col2) {
        return accumulationMatrix[row2 + 1][col2 + 1] - accumulationMatrix[row1][col2 + 1] -
               accumulationMatrix[row2 + 1][col1] + accumulationMatrix[row1][col1];
    }

private:
    vector<vector<int>> accumulationMatrix;
};
// 解法2
class NumMatrix {
private:
    vector<vector<int>> accumulationMatrix;
public:
    NumMatrix(vector<vector<int>> &matrix) {
        accumulationMatrix = vector<vector<int>>(matrix.size() + 1, vector<int>(matrix[0].size() + 1));
        for (int i = 1; i < accumulationMatrix.size(); ++i) {
            for (int j = 1; j < accumulationMatrix[0].size(); ++j) {
                accumulationMatrix[i][j] =
                        accumulationMatrix[i][j - 1] + accumulationMatrix[i - 1][j] - accumulationMatrix[i - 1][j - 1] +
                        matrix[i - 1][j - 1];
            }
        }
    }

    int sumRegion(int row1, int col1, int row2, int col2) {
        return accumulationMatrix[row2 + 1][col2 + 1] - accumulationMatrix[row1][col2 + 1] -
               accumulationMatrix[row2 + 1][col1] + accumulationMatrix[row1][col1];
    }
};
  • 560 和为K的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。

// 解法1:暴力求解
int subarraySum(vector<int> &nums, int k) {
    int ans = 0;
    for (int i = 0; i < nums.size(); ++i) {
        if (nums[i] == k)ans++;
        int sum = nums[i];
        for (int j = i + 1; j < nums.size(); ++j) {
            sum += nums[j];
            if (sum == k)ans++;
        }
    }
    return ans;
}
// 解法2:前缀和+哈希表
// 令s[i]为前i项和, s[j]为前j项和, 如果s[i]=s[j]-k, 则nums[i+1]+nums[i+2]+..+nums[j]=k
// 也就是说, 当我们遍历到第j个元素时, 如果前面存在某个位置的前缀和等于s[j]-k, 则从该位置+1到j就是一个和为k的子数组
int subarraySum(vector<int> &nums, int k) {
    int ans = 0;
    int prefixSum = 0;
    unordered_map<int, int> prefixSum2Count; // key为某个前缀和的值, value为出现次数
    prefixSum2Count[0] = 1; // 未开始遍历时前缀和为0
    for (const int &num: nums) {
        prefixSum += num;
        ans += prefixSum2Count[prefixSum - k];
        prefixSum2Count[prefixSum]++;
    }
    return ans;
}

11.10 练习

  • 566 重塑矩阵

给你一个由二维数组 mat 表示的 m x n 矩阵,以及两个正整数 r 和 c ,分别表示想要的重构的矩阵的行数和列数。
重构后的矩阵需要将原始矩阵的所有元素以相同的 行遍历顺序 填充。
如果具有给定参数的 reshape 操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。

// 解法1:每遍历c个元素,就作为一行插入新数组
vector<vector<int>> matrixReshape(vector<vector<int>> &mat, int r, int c) {
    int m = mat.size();
    int n = mat[0].size();
    if (m * n != r * c)return mat;
    vector<vector<int>> result;
    int cnt = 0;
    vector<int> tmp = {};
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            cnt++;
            tmp.push_back(mat[i][j]);
            if (cnt % c == 0) {
                result.push_back(tmp);
                cnt = 0;
                tmp.clear();
            }
        }
    }
    return result;
}
// 解法2:根据规律找到新数组和旧数组之间下标的对应关系
vector<vector<int>> matrixReshape(vector<vector<int>> &mat, int r, int c) {
    int m = mat.size();
    int n = mat[0].size();
    if (m * n != r * c)return mat;
    vector<vector<int>> result(r, vector<int>(c));
    for (int i = 0; i < m * n; ++i) {
        result[i / c][i % c] = mat[i / n][i % n];
    }
    return result;
}
  • 225 用队列实现栈
// 解法1:两个队列,始终保证最多只有一个队列中有元素,每次top或者pop时都需要进行元素的搬移
class MyStack
{
public:
    queue<int> q1;
    queue<int> q2;
    MyStack()
    {
    }

    // 把src中元素(除了队尾元素)移到dest中
    static void move(queue<int>& dest, queue<int>& src)
    {
        while (src.size() > 1) {
            dest.push(src.front());
            src.pop();
        }
    }

    void push(int x)
    {
        if (q1.empty()) {
            q2.push(x);
        } else {
            q1.push(x);
        }
    }

    int pop()
    {
        int result;
        if (q1.empty()) {
            move(q1, q2);
            result = q2.front();
            q2.pop();
        } else {
            move(q2, q1);
            result = q1.front();
            q1.pop();
        }
        return result;
    }

    int top()
    {
        int result;
        if (q1.empty()) {
            move(q1, q2);
            result = q2.front();
            q2.pop();
            q1.push(result);
        } else {
            move(q2, q1);
            result = q1.front();
            q1.pop();
            q2.push(result);
        }
        return result;
    }

    bool empty()
    {
        return q1.empty() && q2.empty();
    }
};
// 解法2:一个队列
class MyStack
{
public:
    queue<int> q;
    MyStack()
    {
    }

    void push(int x)
    {
        int n = q.size();
        q.push(x);
        for (int i = 0; i < n; ++i) {
            q.push(q.front());
            q.pop();
        }
    }

    int pop()
    {
        int result = q.front();
        q.pop();
        return result;
    }

    int top()
    {
        return q.front();
    }

    bool empty()
    {
        return q.empty();
    }
};
  • 496 下一个更大元素

nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 > nums2 的子集。对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么> 本次查询的答案是 -1 。返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。

// 使用单调栈获取nums2中每个元素及其下一个更大元素,并保存到map中
vector<int> nextGreaterElement(vector<int> &nums1, vector<int> &nums2) {
    unordered_map<int, int> num2largerNum; // 记录nums2中每个数及其下一个更大数
    stack<int> s; // 单调栈,保存nums2中元素
    for (const auto &num: nums2) {
        while (!s.empty() && s.top() < num) {
            num2largerNum.emplace(s.top(), num);
            s.pop();
        }
        s.push(num);
    }
    while (!s.empty()) {
        num2largerNum.emplace(s.top(), -1);
        s.pop();
    }
    vector<int> result(nums1.size(), 0);
    for (int i = 0; i < nums1.size(); ++i) {
        result[i] = num2largerNum[nums1[i]];
    }
    return result;
}
  • 503 下一个更大元素II

给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。
数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

// 解法1:暴力求解,对于每个元素,都遍历n-1次,查看是否有比它大的元素
vector<int> nextGreaterElements(vector<int> &nums) {
    int n = nums.size();
    vector<int> result(n, -1);
    for (int i = 0; i < n; ++i) {
        for (int j = 1; j < n; ++j) {
            if (nums[(i + j) % n] > nums[i]) {
                result[i] = nums[(i + j) % n];
                break;
            }
        }
    }
    return result;
}
// 解法2:单调栈,依次将元素的下标入栈,如果当前遍历的元素nums[i]大于栈顶下标top对应的元素,则栈顶下标对应元素的下一个更大元素就是当前元素,即res[top]=nums[i]。然后将栈顶下标出栈
// 参考739 每日温度
vector<int> nextGreaterElements(vector<int> &nums) {
    int n = nums.size();
    vector<int> res(n, -1);
    stack<int> indexStack;
    for (int i = 0; i < 2 * n - 1; ++i) { // 对于每个元素来说,需要遍历一遍后面n-1个元素,因此总共需要遍历2*n-1个元素(通过取模操作实现循环)
        int index = i % n;
        while (!indexStack.empty() && nums[indexStack.top()] < nums[index]) {
            res[indexStack.top()] = nums[index];
            indexStack.pop();
        }
        indexStack.push(index);
    }
    return res;
}
  • 217 存在重复元素

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。

// 解法1:暴力求解
bool containsDuplicate(vector<int> &nums) {
    unordered_multiset<int> ums;
    for (const int &num: nums) {
        ums.insert(num);
    }
    for (const int &num: nums) {
        if (ums.count(num) > 1)return true;
    }
    return false;
}
// 解法2:先排序,然后再判断相邻元素是否相等
bool containsDuplicate(vector<int> &nums) {
    sort(nums.begin(), nums.end());
    for (int i = 0; i < nums.size() - 1; ++i) {
        if (nums[i] == nums[i + 1])return true;
    }
    return false;
}
// 解法3:使用哈希表快速查找是否已经存在相同元素
bool containsDuplicate(vector<int> &nums) {
    unordered_set<int> s;
    for (const int &num: nums) {
        if (s.find(num) != s.end()) {
            return true;
        }
        s.insert(num);
    }
    return false;
}
  • 697 数组的度

给定一个非空且只包含非负数的整数数组 nums,数组的 度 的定义是指数组里任一元素出现频数的最大值。
你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。

// 使用map记录每个元素及其出现的次数、第一次出现下标、最后一次出现下标
int findShortestSubArray(vector<int> &nums) {
    int n = nums.size();
    unordered_map<int, vector<int>> map;
    for (int i = 0; i < n; ++i) {
        if (map.count(nums[i])) {
            map[nums[i]][0]++;
            map[nums[i]][2] = i;
        } else {
            map[nums[i]] = {1, i, i};
        }
    }
    int maxCount{0};
    int interval{n};
    for (auto i = map.begin(); i != map.end(); ++i) {
        auto it = (*i).second;
        if (it[0] >= maxCount) {
            if (it[0] == maxCount) {
                interval = min(interval, it[2] - it[1] + 1);
            } else {
                interval = it[2] - it[1] + 1;
            }
            maxCount = it[0];
        }
    }
    return interval;
}
  • 594 最长和谐子序列

和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。
现在,给你一个整数数组 nums ,请你在所有可能的子序列中找到最长的和谐子序列的长度。
数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。

// 解法1:将元素插入multiset中,遍历每个num的同时查看num-1和num+1的个数
int findLHS(vector<int> &nums) {
    unordered_multiset<int> set;
    for (const int &num: nums) {
        set.insert(num);
    }
    int ans{0};
    for (const int &num: set) {
        if (!set.count(num - 1) && !set.count(num + 1))continue;
        int count = max(set.count(num) + set.count(num - 1), set.count(num) + set.count(num + 1));
        ans = max(ans, count);
    }
    return ans;
}
// 解法2:解法1的变形,将元素插入multiset中,遍历每个num的同时只需要查看num-1个数,不需要查看num+1的个数
int findLHS(vector<int> &nums) {
    unordered_multiset<int> set;
    for (const int &num: nums) {
        set.insert(num);
    }
    int ans{0};
    for (const int &num: set) {
        if (!set.count(num - 1))continue;
        int count = set.count(num) + set.count(num - 1);
        ans = max(ans, count);
    }
    return ans;
}
// 解法3:先对数组进行排序,得到多个由相同元素构成的子序列,例如[1],[2,2,2],[3,3],[5],[7]
// 寻找两个元素相差1的相邻子序列,这两个子序列即构成了原数组的一个和谐子序列
int findLHS(vector<int> &nums) {
    int ans{0};
    sort(nums.begin(), nums.end());
    int begin{0};
    for (int end = 1; end < nums.size(); end++) {
        while (nums[end] - nums[begin] > 1) {
            begin++;
        }
        if (nums[end] - nums[begin] == 1) {
            ans = max(ans, end - begin + 1);
        }
    }
    return ans;
}
  • 287 寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

// 解法1:参考448,但是不符合题意,空间复杂度为O(N)
int findDuplicate(vector<int> &nums) {
    int n = nums.size();
    vector<int> count(n + 1, 0);
    for (int i = 0; i < n; i++) {
        count[nums[i]]++;
    }
    for (int i = 1; i < n + 1; i++) {
        if (count[i] > 1) {
            return i;
        }
    }
    return 0;
}
// 解法2:参考448,但是不符合题意,修改了原数组
int findDuplicate(vector<int> &nums) {
    for (int i = 0; i < nums.size(); i++) {
        int position = abs(nums[i]) - 1;
        if (nums[position] > 0) {
            nums[position] = -nums[position];
        } else {
            return position + 1;
        }
    }
    return 0;
}
// 解法3:二分法,统计数组元素在[1,n/2]之间的个数,如果个数>n/2,则说明重复的数在[1,n/2]之间,否则说明重复的数在(n/2,n]之间
int findDuplicate(vector<int> &nums) {
    int min = 1;
    int max = nums.size() - 1;
    while (min < max) {
        int mid = (min + max) / 2;
        int count = 0;
        for (const int &num: nums) {
            if (num >= min && num <= mid) {
                count++;
            }
        }
        if (count > mid - min + 1) { // 如果数组中处于[min,mid]范围的元素个数大于mid-min+1的话,说明重复的数在这个范围内
            max = mid;
        } else {
            min = mid + 1;
        }
    }
    return min;
}
// 解法4:快慢指针,将数组看作链表,下标为i的节点指向下标为nums[i]的节点
// 因为元素的取值范围是[1,n],而数组的下标范围是[0,n],因此这个链表肯定存在环!!!(nums[i]=i表明下标为i的元素指向自己,也算一个环)
// 又因为重复元素指向同一个下标,因此入环点的下标就是重复的元素
int findDuplicate(vector<int> &nums) {
    int fast = 0;
    int slow = 0;
    do {
        fast = nums[nums[fast]];
        slow = nums[slow];
    } while (fast != slow);
    fast = 0;
    while (fast != slow) {
        fast = nums[fast];
        slow = nums[slow];
    }
    return slow;
}
  • 141 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。如果链表中存在环 ,则返回 true 。 否则,返回 false 。

// 解法1:哈希表,如果某个节点在哈希表中已经存在,则说明有环
bool hasCycle(ListNode *head) {
    unordered_set<ListNode *> set;
    while (head != nullptr) {
        if (set.count(head))return true;
        set.insert(head);
        head = head->next;
    }
    return false;
}
// 解法2:快慢指针:如果没有环,则快指针一定会走到链表结尾;如果有环,则快慢指针一定会相遇
bool hasCycle(ListNode *head) {
    if (head == nullptr || head->next == nullptr)return false;
    ListNode *slow = head;
    ListNode *fast = head->next;
    while (slow != fast) {
        if (fast == nullptr || fast->next == nullptr)return false;
        fast = fast->next->next;
        slow = slow->next;
    }
    return true;
}
  • 142 环形链表II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

// 解法1:哈希表
ListNode *detectCycle(ListNode *head) {
    unordered_set<ListNode *> set;
    while (head != nullptr) {
        if (set.count(head)) {
            return head;
        }
        set.insert(head);
        head = head->next;
    }
    return nullptr;
}
// 解法2:快慢指针
// 如果链表有环,则fast和slow一定会在环上的某个节点相遇,此时2x-x=n*b(b是环的节点个数),说明slow已经走了n*b步
// 又因为只要slow走的步数满足a+n*b(a是进入环之前的链表节点个数)就可以到达环的入口(即slow只要再走a步就可以到达环的入口)
// 因此将fast重新指向链表的head节点(此时fast也是走a步即可到达环的入口),并每次前进1步
// 当fast和slow再次相遇时,它们一定已经走了a步,并且相遇的点就是环的入口
ListNode *detectCycle(ListNode *head) {
    ListNode *slow = head, *fast = head;
    do {
        if (fast == nullptr || fast->next == nullptr)return nullptr;
        fast = fast->next->next;
        slow = slow->next;
    } while (slow != fast);
    fast = head;
    while (slow != fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}
  • 313 超级丑数

超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。
给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。

// 解法1:优先队列(小根堆)
// 每个丑数都是由前面的丑数分别与primes[i]相乘得到(第一个丑数是1)
// a) 起始先将最小丑数 1 放入队列
// b) 每次从队列取出最小值 x,然后将 x 所对应的丑数 x∗primes[i] 进行入队。
// c) 对步骤 2 循环多次,第 n 次出队的值即是答案。
int nthSuperUglyNumber(int n, vector<int> &primes) {
    set<int> ans; // 不能使用vector,会超时,使用set实现有序
    priority_queue<int, vector<int>, greater<>> priorityQueue;
    priorityQueue.push(1);
    while (!priorityQueue.empty()) {
        int top = priorityQueue.top();
        priorityQueue.pop();
        if (ans.count(top))continue; // 跳过重复的丑数
        ans.insert(top);
        if (ans.size() == n)break;
        for (const int &prime: primes) {
            if (top > INT_MAX / prime)continue; // 跳过超出范围的丑数
            priorityQueue.push(prime * top);
        }
    }
    return *(--ans.end()); // 取出最后一个元素,即为第n个丑数
}
// 解法2:动态规划,将每个primes[i]乘以某个已有丑数,所有乘积的最小值即为要找的下一个丑数(每个primes[i]具体乘以哪个丑数,用下标来保存)
int nthSuperUglyNumber(int n, vector<int> &primes) {
    vector<int> uglyNumbers(n + 1); // 丑数序列
    uglyNumbers[1] = 1; // 第1个丑数为1
    vector<int> multiplications(primes.size()); // 将 每个primes[i]分别乘以某个已有丑数, 所有乘积的最小值就是要找的下一个丑数
    vector<int> indexes(primes.size(), 1); // 每个primes[i]要乘以的丑数的下标(第2个丑数由所有primes[i]都乘以1的最小值得到)
    for (int i = 2; i < n + 1; ++i) {
        int i_uglyNumber = INT_MAX; // 第i个丑数
        for (int j = 0; j < primes.size(); ++j) {
            if (primes[j] > INT_MAX / uglyNumbers[indexes[j]])continue;
            // 每个primes[j]乘以对应下标的丑数,这些乘积里的最小值即为下一个丑数
            multiplications[j] = primes[j] * uglyNumbers[indexes[j]];
            i_uglyNumber = min(i_uglyNumber, multiplications[j]);
        }
        uglyNumbers[i] = i_uglyNumber;
        for (int j = 0; j < primes.size(); ++j) {
            // 如果第i个丑数是由primes[j]乘以下标为indexes[j]的丑数得到的,那么找下一个丑数时,primes[j]就得乘以下标为indexes[j]+1的丑数了
            if (multiplications[j] == i_uglyNumber) {
                indexes[j]++;
            }
        }
    }
    return uglyNumbers[n];
}
  • 870 优势洗牌

给定两个长度相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。

// 解法1:贪心算法,对于每个nums2[i],将nums1数组中大于nums2[i]的最小值放在下标为i的位置;如果nums1中不存在大于nums2[i]的元素,则使用整个数组的最小值放在下标为i的位置
// 由于每次都要遍历nums1数组,在数据量特别大时会超时
vector<int> advantageCount(vector<int> &nums1, vector<int> &nums2) {
    vector<int> result(nums1.size());
    std::sort(nums1.begin(), nums1.end());
    for (int i = 0; i < nums2.size(); ++i) {
        if (nums1.back() <= nums2[i]) {
            result[i] = nums1.front();
            nums1.erase(nums1.begin());
        } else {
            for (auto it = nums1.begin(); it != nums1.end(); ++it) {
                if (*it > nums2[i]) {
                    result[i] = *it;
                    nums1.erase(it);
                    break;
                }
            }
        }
    }
    return result;
}
// 解法2:贪心算法,将nums1和nums2排序,然后比较最小值
// 如果nums1的最小值大于nums2的最小值,则将nums1的最小值放在nums2最小值所在下标处
// 如果nums1的最小值小于nums2的最小值,则将nums1的最小值放在nums2最大值所在下标处
vector<int> advantageCount(vector<int> &nums1, vector<int> &nums2) {
    int n = nums1.size();
    sort(nums1.begin(), nums1.end()); // 将nums1按照从小到大进行排序
    vector<int> index(n);
    std::iota(index.begin(), index.end(), 0); // 下标数组[0,n-1]
    // 将nums2的下标按照对应元素的值从小到大进行排序,即:如果nums2[i] < nums2[j],则i排在j的前面
    sort(index.begin(), index.end(), [&](int i, int j) {
        return nums2[i] < nums2[j];
    });
    vector<int> ans(n);
    // 将nums1的最小值和nums2的最小值进行比较
    // 如果nums1的最小值大于nums2的最小值,则将nums1的最小值放在与nums2的最小值对应的下标处
    // 如果nums1的最小值小于nums2的最小值,则将nums1的最小值放在nums2的最大值对应的下标处
    int left = 0, right = n - 1;
    for (const int &num: nums1) {
        // index[left]是nums2的最小值所在的下标,index[right]是nums2的最大值所在的下标
        int currentIndex = num > nums2[index[left]] ? index[left++] : index[right--];
        ans[currentIndex] = num;
    }
    return ans;
}
posted @ 2024-07-15 14:58  dengkang1122  阅读(4)  评论(0编辑  收藏  举报