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
:支持重复元素的setmap
:有序映射或有序表
,在set的基础上加上映射关系,可以对每个元素key存一个值valuemultimap
:支持重复元素的map
- 未排序的关联容器:
对每个关联容器实现了哈希版本
unordered_set
:哈希集合
,支持O(1)插入、删除、查找元素,常用于快速查询一个元素是否在这个容器内unordered_multiset
:支持重复元素的unordered_setunordered_map
:哈希映射或哈希表
,在unordered_set的基础上加上映射关系,可以对每一个元素key存一个值valueunordered_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;
}