排序算法
冒泡排序
把数组排成最小的数
(贪心)
class Solution {
public:
string minNumber(vector<int>& nums) {
sort(nums.begin(), nums.end(), [](int a, int b) {
string t1 = to_string(a);
string t2 = to_string(b);
return t1 + t2 < t2 + t1;
});
string ans;
for (int num : nums) {
ans += to_string(num);
}
return ans;
}
};
移动零
(冒泡排序思想)
第一种最朴素的思想就是当遇到零的时候需要将零移至数组的最后,因为需要保证数组的非零元素的相对顺序不能改变,所以只能相邻的两个元素交换,这就就可以利用冒泡排序的思想,遇到一个零的时候,就将零移动到未排序数组的最后一个,这样就可以将所有的零移动到数组的最后也同时保证了数组中数的相对顺序。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int zeros = 0;
for (int i = 0; i < nums.size() - zeros; i ++) {// 对前nums.size()-zeros进行排序
if (nums[i] == 0) {// 如果数字等于0就将0移动到未排序数组的最后
for (int j = i; j < nums.size() - zeros - 1; j ++) {
swap(nums[j], nums[j + 1]);
}
zeros ++;
i --;// 因为循环结束要i++,而刚刚因为nums[i]交换过,所以要重新检查nums[i]是否为0
}
}
}
};
(双指针)
要将零全部放在数的后半部分,换一个思路就是将所有的非零的数放在数组的前半部分。
解决方法:遍历一遍数组,将非零的元素依次放在数组的前面。遍历完数组后数组的后半部用零填充。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int j = 0;
for (int i = 0; i < nums.size(); i ++) {
if (nums[i] != 0) {
nums[j ++] = nums[i];
}
}
while (j < nums.size()) {
nums[j++] = 0;
}
}
};
(快排思想)
将nums[i]!=0
的数放在0
的左边,nums[i]==0
的数放在0
的右边
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int j = 0;
for (int i = 0; i < nums.size(); i ++) {
if (nums[i] != 0) {
swap(nums[i], nums[j]);
j ++;
}
}
}
};
选择排序
数组中的第k个最大元素
(sort)
直接无脑降序sort
,然后返回nums[k - 1]
缺点:改变了原数组
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(), nums.end(), greater<int>());
return nums[k - 1];
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(1)
(堆)
第二种就是利用优先队列的性质(大根堆和小根堆都可),将所有的数字放入大根堆中,也就是将数字进行了堆排序。最后通过pop()
前k个数,留下来的堆顶的数就是第k大的数。
缺点:占用的空间过大
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> q;
for (int num : nums) {
q.push(num);
}
while (--k) {
q.pop();
}
return q.top();
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(n)
(堆优化1)维护k大小的堆
在堆原有的结构优势上,为了节省一点空间,可以只维护一个大小为k的小根堆。
将k个数字放入小根堆中,然后将数组中数和堆顶元素比较,如果数组中的数较大,就将堆顶元素pop
将数组中的数放入堆中。经过遍历一遍数组就可以将前k大的数放入小根堆中,这是堆顶的元素就是第k大的元素。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> q;
for (int i = 0; i < k; i ++) {
q.push(nums[i]);
}
for (int i = k; i < nums.size(); i ++) {
if (nums[i] > q.top()) {
q.pop();
q.push(nums[i]);
}
}
return q.top();
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(k)
(堆优化2)维护k+1大小的堆
在堆优化1的基础上,为了代码的简便,可以维护一个大小为k+1
的堆。这样在将k个元素放入堆中后,在放入第k+1
个元素的时候,每放入一个元素就删除一个元素,这样就可以保证堆中的元素还是k
个,同时堆中的元素也是已经排好序的。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> q;
for (int i = 0; i < k; i ++) {
q.push(nums[i]);
}
for (int i = k; i < nums.size(); i ++) {
q.push(nums[i]);
q.pop();
}
return q.top();
}
// 另一种写法,合并成一个循环
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> q;
for (int i = 0; i < nums.size(); i ++) {
q.push(nums[i]);
if (q.size() == k + 1) q.pop();
}
return q.top();
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(k)
(堆优化3)分情况建堆
程序的所用的空间大小取决于k
的大小,但是k不能确定。当有100个数的时候k=3
,就可以使用小根堆维护前k
个大的元素。但是如果k=97
这是如果还是维护一个大小为97的堆就得不偿失了,这时就可以反过来想可以找到第4小的元素即可,这样只用维护一个大小为4的大根堆,这样就最大限度上节省了空间。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int len = nums.size();
if (k + k <= len) {// 小根堆
priority_queue<int, vector<int>, greater<int>> q;
for (int i = 0; i < k; i ++) {
q.push(nums[i]);
}
for (int i = k; i < nums.size(); i ++) {
q.push(nums[i]);
q.pop();
}
return q.top();
} else {// 大根堆
int cap = len - k + 1;
priority_queue<int> q;
for (int i = 0; i < cap; i ++) {
q.push(nums[i]);
}
for (int i = cap; i < nums.size(); i ++) {
q.push(nums[i]);
q.pop();
}
return q.top();
}
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(k)
(二分)
经过观察可以发现,第k大的元素应该大于数组中的其他元素的个数应该大于等于k,而其他的第i大的元素应该大于数组中的元素个数大于等于i,这样就找到数组中划分数组的二段性,可以利用二分解决这个问题。
优点:占用空间小,不改变原数组
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int l = INT_MAX, r = INT_MIN;
for (int num : nums) {
l = min(l, num);
r = max(r, num);
}
// 第k大数就是数组中正好有k个数大于等于该数字
while (l < r) {
int mid = l + r + 1 >> 1;
int cnt = 0;
for (int num : nums) {
if (num >= mid) cnt ++;
}
if (cnt >= k) l = mid;
else r = mid - 1;
}
return l;
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(1)
(快排+二分)
快排的思想是每一次选取一个数,然后操作数组使得这个数的左边都不大于该数字,这个数的右边的数都不小于这个数,这样这个数就找到了自己在数组中的位置。利用这点,我们又知道第k大的元素的索引在正序数组中是nums.size() - k
,所以只要找到一个快排好的数字的索引是nums.size() - k
,就找到了该数字。
class Solution {
public:
int partition(vector<int>& nums, int l, int r) {// 双指针
int pos = l;
while (l < r) {
while (l < r && nums[r] >= nums[pos]) r --;
while (l < r && nums[l] <= nums[pos]) l ++;
swap(nums[l], nums[r]);
}
swap(nums[l], nums[pos]);
return l;
}
// int partition1(vector<int>& nums, int l, int r) {// 覆盖
// int x = nums[l], i = l;
// for (int j = l + 1; j <= r; ++j) {
// if (nums[j] <= x) {
// swap(nums[++i], nums[j]);
// }
// }
// swap(nums[i], nums[l]);
// return i;
// }
// int partition2(vector<int>& nums, int l, int r) {// 挖洞
// int key = nums[l];
// while(l < r){
// while(l < r && nums[r] >= key)
// r--;
// nums[l] = nums[r];
// while(l < r && nums[l] <= key)
// l++;
// nums[r] = nums[l];
// }
// nums[l] = key;
// return l;
// }
int findKthLargest(vector<int>& nums, int k) {
int len = nums.size();
int target = len - k;
int l = 0, r = len - 1;
while (l < r) {
int pos = partition1(nums, l, r);
if (pos == target) return nums[pos];
else if(pos < target) l = pos + 1;
else r = pos - 1;
}
return nums[l];
}
};
- 时间复杂度O(nlogn)
- 空间复杂度O(1)
插入排序
对链表进行插入排序
(链表)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* end = head;
ListNode* cur = head->next;
while (cur != nullptr) {
if (cur->val <= end->val) {
end = end->next;
} else {
ListNode* prev = dummy;
while (prev->next->val <= cur->val) {
prev = prev->next;
}
end->next = cur->next;
cur->next = prev->next;
prev->next = cur;
}
cur = end->next;
}
return dummy->next;
}
};
希尔排序
相对次序
(希尔+哈希)
class Solution {
public:
void ShellSort(vector<int>& nums) {
int n = nums.size();
int gap = n / 2;
while (gap >= 1) {
for (int i = gap; i < n; i ++) {
int tmp = nums[i];
int j = i - gap;
while (j >= 0 && nums[j] < tmp) {
nums[j + gap] = nums[j];
j -= gap;
}
nums[j + gap] = tmp;
}
gap /= 2;
}
}
vector<string> findRelativeRanks(vector<int>& score) {
vector<int> nums(score.begin(), score.end());
ShellSort(nums);
unordered_map<int, int> hash;
for (int i = 0; i < score.size(); i ++) {
hash.insert({nums[i], i + 1});
}
vector<string> ans;
for (int i = 0; i < score.size(); i ++) {
if (score[i] == nums[0]) ans.push_back("Gold Medal");
else if (score[i] == nums[1]) ans.push_back("Silver Medal");
else if (score[i] == nums[2]) ans.push_back("Bronze Medal");
else ans.push_back(to_string(hash[score[i]]));
}
return ans;
}
};
堆排序
合并K个升序链表
(多路归并)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* dummy = new ListNode(0);
ListNode* head = dummy;
while (true) {
int minIndex = -1;
ListNode* minNode = nullptr;
for (int i = 0; i < lists.size(); i ++) {
if (lists[i] == nullptr)
continue;
if (minNode == nullptr || minNode->val > lists[i]->val) {
minNode = lists[i];
minIndex = i;
}
}
if (minIndex == -1) break;
head->next = lists[minIndex];
head = head->next;
lists[minIndex] = lists[minIndex]->next;
}
return dummy->next;
}
};
(多路归并-堆优化)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
struct cmp {
bool operator() (ListNode* l1, ListNode* l2) {
return l1->val > l2->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*, vector<ListNode*>, cmp> q;
for (auto node : lists) {
if (node)
q.push(node);
}
ListNode* dummy = new ListNode(0);
ListNode* tail = dummy;
while (!q.empty()) {
ListNode* minNode = q.top();
q.pop();
tail->next = minNode;
tail = tail->next;
if (minNode->next != nullptr) {
q.push(minNode->next);
}
}
return dummy->next;
}
};
(多路归并链表)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);
ListNode* tail = dummy;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
tail->next = l1 == nullptr ? l2 : l1;
return dummy->next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* ans = nullptr;
for (auto list : lists) {
ans = mergeTwoLists(list, ans);
}
return ans;
}
};
(两两合并链表)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoList(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {
return l2;
}
if (l2 == nullptr) {
return l1;
}
if (l1->val < l2->val) {
l1->next = mergeTwoList(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoList(l1, l2->next);
return l2;
}
}
ListNode* merge(vector<ListNode*>& lists, int l, int r) {
if (l == r) {
return lists[l];
}
int mid = l + (r - l) / 2;
ListNode* l1 = merge(lists, l, mid);
ListNode* l2 = merge(lists, mid + 1, r);
return mergeTwoList(l1, l2);
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) return nullptr;
return merge(lists, 0, lists.size() - 1);
}
};
最小的k个数
(二分)
使用二分可以找到,第k小的数字是多少,然后将数组中小于该数字的k个数字组合而成
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& nums, int k) {
if (k == 0) return {};
int l = INT_MAX, r = -1;
for (int num : nums) {
l = min(l, num);
r = max(r, num);
}
// 找到第k小的数
while (l < r) {
int mid = l + r >> 1;
int cnt = 0;
for (int num : nums) {
if (mid >= num)
cnt ++;
}
if (cnt >= k) r = mid;
else l = mid + 1;
}
// 将<第k小的数放入答案中
vector<int> ans;
for (int num : nums) {
if (num < l) {
ans.push_back(num);
}
}
// 将等于第k小的数放入答案中直至size等于k
for (int num : nums) {
if (num == l) {
ans.push_back(num);
}
if (ans.size() == k) break;
}
return ans;
}
};
(堆优化)
维护一个大小为k的大根堆,在经过nums
数组的淘汰后,剩下的数字就前k个小的数了。
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& nums, int k) {
if (k == 0) return {};
priority_queue<int> q;
for (int i = 0; i < k; i ++) {
q.push(nums[i]);
}
for (int i = k; i < nums.size(); i ++) {
if (nums[i] <= q.top()) {
q.pop();
q.push(nums[i]);
}
}
vector<int> ans;
while (!q.empty()) {
ans.push_back(q.top());
q.pop();
}
return ans;
}
};
(快排)
第k小的数的索引应该处于k - 1
上,所以可以使用快排配合二分找出所以值为k - 1
,这是在k -1
的左边也已经都是< nums[k - 1]
的数了。
class Solution {
public:
int quickSort(vector<int>& nums, int l, int r) {
int key = nums[l];
int keyi = l;
while (l < r) {
while (l < r && nums[r] >= key) r --;
while (l < r && nums[l] <= key) l ++;
swap(nums[l], nums[r]);
}
swap(nums[l], nums[keyi]);
return l;
}
vector<int> getLeastNumbers(vector<int>& nums, int k) {
if (k == 0) return {};
int target = k - 1;
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = quickSort(nums, l, r);
if (mid == target) break;
else if (mid > target) r = mid - 1;
else l = mid + 1;
}
vector<int> ans;
for (int i = 0; i <= target; i ++) {
ans.push_back(nums[i]);
}
return ans;
}
};
(桶排序思想)
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
if (k == 0) return {};
// 算出桶的最大容量
int maxSize = 0;
for (int num : arr) {
maxSize = max(maxSize, abs(num));
}
// 将元素放入桶中
vector<list<int>> bucket(2 * maxSize + 1);
for (int num : arr) {
bucket[maxSize + num].push_back(num);
}
// 从前往后将元素放入ans中
vector<int> ans;
for (int i = 0; i <= 2 * maxSize; i ++) {
while (!bucket[i].empty()) {
ans.push_back(i - maxSize);
k --;
if (k == 0) {
return ans;
}
bucket[i].pop_back();
}
}
return ans;
}
};
前K个高频元素
(sort)
最暴力也是最方便的方式就是利用库函数sort
直接对数组中数组进行排序,然后取出前k个元素即可。
- 时间复杂度O(nlogn)
- 空间复杂度O(1)
补充:另外只要使用排序算法直接对数组中的数进行排序,最好的情况都是O(nlogn)了,所以后面就不将各种排序算法都一一罗列出来了。
(手写小根堆)
统计前K个高频的元素,这样描述一个数组中的元素,就是让数组中的数字有了优先级的概念,也就是先后之分,所以就可以想到使用堆(优先队列)来解决这个问题。
1.这个题目和直接算出第k个最大元素的区别在于:被题要求出前K个高频元素,而不是第K个高频元素,所以需要返回一个数组,而不是一个数。这就需要我们先将每一个数出现的频率预处理好,然后再进行后续处理。解决方法:可以使用一个unordered_map
哈希表来统计数字出现的频率。
2.然后就是就是将k个数放入数组中,然后建小根堆。
这里就有问题了。为什么要建小根堆呢?不是要统计前k个高频的元素吗,直接将数字放入数组中建一个大根堆然后取出前k个元素不就完事了吗?
答:其实这样做也是可以的,因为这直接就是一个堆排序,前面也说了排序算法都是可以的,但是这样的话,时间复杂度不就还是O(nlogn),就没有体现出堆的真正的优势了。
**解决方法:**使用小根堆,虽然看起来好像反其道而行,但是却有奇效。因为如果使用小根堆,就可以只维护一个大小为k的小根堆。因为维护的是小根堆,所以经过不断地淘汰堆中的最小的元素就可以使得剩下的元素是数组中的最大的k个元素,所以维护一个小根堆其实是可以方便的找到堆中的最小元素,然后删除掉最小元素。
实现的方法:当堆中的元素个数到达k个的时候,就可以直接判断堆顶元素是否在某种性质上(比较两个元素的大小的性质)比新元素要大,如何满足就可以加入该元素,然后删除掉堆顶最小的元素。
口诀:小根堆保大删小,大根堆保小删大。
当然还有一种维护堆的方法:就是维护一个大小为k+1的小根堆,这样堆的大小为k的时候,直接可以加入第k+1个元素,然后再删除堆顶的元素,这时候堆中还是只有k个元素。为什么可以直接无脑的加入第k个元素?因为再加入第k+1个元素的时候,由于堆的性质,就可以自动的排序,就不用维护大小为k的堆一样还需要判断堆顶元素和加入元素的大小。如果直接利用堆的性质,就算是加入的元素比堆顶的元素还要小,堆自动排序后该元素还是在堆顶,一样要被删除掉。
class Solution {
public:
typedef pair<int, int> PII;
// 向上调整
void shiftUp(vector<PII>& arr, int pos) {
while (pos > 0) {
int parent = (pos - 1) / 2;
if (arr[parent].second > arr[pos].second) {
swap(arr[parent], arr[pos]);
}
pos = parent;
}
}
// 向下调整
void shiftDown(vector<PII>& arr, int pos, int n) {
while (2 * pos + 1 < n) {
int child = 2 * pos + 1;
if (child + 1 < n && arr[child + 1].second < arr[child].second) {
child ++;
}
if (arr[pos].second <= arr[child].second) {
break;
}
swap(arr[pos], arr[child]);
pos = child;
}
}
vector<int> topKFrequent(vector<int>& nums, int k) {
// 统计数字出现的次数
unordered_map<int, int> hash;
for (int num : nums) {
hash[num] ++;
}
// 将前k个数放在数组中,并将数组整理成小根堆
vector<PII> arr;
auto it = hash.begin();
for (int i = 0; i < k; i ++) {
arr.push_back(*it);
shiftUp(arr, i);
it ++;
}
// 维护一个大小为k的小根堆
while (it != hash.end()) {
if (it->second > arr[0].second) {
arr[0] = *it;
// pop()
shiftDown(arr, 0, arr.size());
}
it ++;
}
// // 维护一个大小为k+1的小根堆
// while (it != hash.end()) {
// if (it->second > arr[0].second) {
// // push()
// arr.push_back(*it);
// shiftUp(arr, arr.size() - 1);
// // pop()
// swap(arr[0], arr.back());
// arr.pop_back();
// shiftDown(arr, 0, arr.size());
// }
// it ++;
// }
// 将堆中的数据放入ans数组中
vector<int> ans;
for (int i = 0; i < arr.size(); i ++) {
ans.push_back(arr[i].first);
}
return ans;
}
};
- 时间复杂度O(nlogk)
- 空间复杂度O(n)
(堆优化)
如果不想手写一个堆,也可以使用C++提供的priority_queue
也是可以的,但是要注意priority_queue
默认的是大根堆,如果想要变成小根堆还需要添加一些参数。因为使用的是unordered_map
来统计数组及其出现的频率,所以哈希表中的元素保存的是pair<int,int>
,所以priority_queue
中的元素也是pair<int,int>
,并且因为需要比较两个pair
,所以需要自己写一个结构体来重载operator()
,来保证堆中的优先级。
class Solution {
public:
struct cmp {
bool operator() (pair<int, int>& a, pair<int, int>& b) {
return a.second > b.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 记录每一个数的频率
unordered_map<int, int> hash;
for (int num : nums) {
hash[num] ++;
}
// 将数字放入队列中,维护一个大小为k的小根堆,按数字的频率。
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> q;
for (auto e : hash) {
if (q.size() == k) {
if (e.second > q.top().second) {// 每一次将大频率出现的数组加入堆中
q.push(e);
q.pop();
}
} else {
q.push(e);
}
}
// // 维护一个大小为k + 1的小根堆
// for (auto e : hash) {
// if (q.size() < k) {
// q.push(e);
// } else {
// q.push(e);
// q.pop();
// }
// }
vector<int> ans;
while (!q.empty()) {
ans.push_back(q.top().first);
q.pop();
}
return ans;
}
};
- 时间复杂度O(nlogk)
- 空间复杂度O(n)
(快速排序+二分)
要找前k高频的元素,可以先找出第k高频的元素。
如果使用快排后找到第k大的元素,因为该元素的左右两边分别是比该元素小的元素和比该元素大的元素,所以第k高频的元素后面就是前k高频的元素。
解决方法:第k高频的元素的下标为nums.size() - k
,可以使用二分,判断一次随机的快排确定的一个元素的位置,然后根据该元素的位置判断下面需要排序的范围。
class Solution {
public:
typedef pair<int, int> PII;
// 快排,将arr[l]这个数放在该数字应该在的位置上
int quickSort(vector<PII>& arr, int l, int r) {
auto key = arr[l];
int keyi = l;
while (l < r) {
while (l < r && arr[r].second >= key.second) r --;
while (l < r && arr[l].second <= key.second) l ++;
swap(arr[l], arr[r]);
}
swap(arr[l], arr[keyi]);
return l;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
// 统计数字出现的频率
unordered_map<int, int> hash;
for (int num : nums) {
hash[num] ++;
}
// 将哈希表的数字和出现的频率放入数组中计算
vector<PII> arr(hash.begin(), hash.end());
int target = arr.size() - k;
// 二分搜索出第k大的元素的位置
int l = 0, r = arr.size() - 1;
while (l < r) {
int mid = quickSort(arr, l, r);
if (mid == target) break;
else if (mid > target) r = mid - 1;
else l = mid + 1;
}
// 将前K个高频元素放入ans中
vector<int> ans;
for (int i = target; i < arr.size(); i ++) {
ans.push_back(arr[i].first);
}
return ans;
}
};
- 时间复杂度O(n2)–>最坏的情况
- 平均时间复杂度O(n)–>加logn次半数组,n + n/2 + n/4 + …
- 空间复杂度O(n)
(桶排序)
利用桶排序可以将数组中的数字出现的频率作为数组的下标,这样数组中从前往后只要遇到元素就是出现频率从小到大的元素。
如果数字出现的频率是相同的怎么办?
可以在vector
中存放一个list
,让出现频率相同的元素挂在同一个链表上,取出节点的时候,也是在链表上取出节点。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
// 统计数字出现的频率
unordered_map<int, int> hash;
for (int num : nums) {
hash[num] ++;
}
// 根据数字出现的频率来对应数组的下标,将数字放在对应下标的位置上
// 并且因为可能会有数字出现的频率是相同的,所以在vector中再套一个list
vector<list<int>> arr(nums.size() + 1);
for (auto e : hash) {
int fre = e.second;
int num = e.first;
arr[fre].push_back(num);
}
// 根据频率下标,从后往前的遍历数组,知道装满k个数字
vector<int> ans;
for (int i = arr.size() - 1; i >= 0 && k != 0; i --) {
while (!arr[i].empty()) {
ans.push_back(arr[i].back());
arr[i].pop_back();
k --;
}
}
return ans;
}
};
- 时间复杂度O(n)
- 空间复杂度O(n)
数据流中的中位数
(大根堆+小根堆)
可以发现要取得数组中的中位数就是将数组切成两半后,如果数组中的个数为奇数的话,就是前半部分数组的最后一个数,如果数组中的个数为偶数的话,就是前半部分数组的最后一个数和后半部分数组的第一个数,两个数的平均值。
因为两个数组要保证两个数组都要序,并且后面一个数组的第一个数要比前面一个数组的最后一个数要大。连个数组要有序可以使用堆的结构优势直接使得数组中的数有序,但是要保证前面一个数组的最大数比后一个数组的最小数要大就不太好办了。
可以这样想,如果想要直接拿出前半个数组的最大数的话,就需要用大根堆来维护。如果想要直接拿出后半个数组的最小数的话,就需要用小根堆来维护。而要使得小根堆的堆顶一定要比大根堆的数字要大,就可以直接将大根堆的堆顶元素拿出来放进小根堆中,这样就可以使得小根堆中的元素比大根堆中的所有元素都大,可以每次将放入小根堆中的元素都放入大根堆中“过滤”一下。
最后要记得,大根堆中的元素个数一定要大于等于小根堆中的元素个数,也就是在奇数个元素的时候,优先将元素放入大根堆中。
class MedianFinder {
private:
priority_queue<int> maxHeap;
priority_queue<int, vector<int>, greater<int>> minHeap;
int size;
public:
/** initialize your data structure here. */
MedianFinder() {
size = 0;
}
void addNum(int num) {
maxHeap.push(num);
minHeap.push(maxHeap.top());
maxHeap.pop();
size ++;
if (size % 2 != 0) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
double findMedian() {
if (size % 2 == 0) {
return (minHeap.top() + maxHeap.top()) / 2.0;
} else {
return maxHeap.top() / 1.0;
}
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
(手写大小堆)
class MedianFinder {
private:
vector<int> maxHeap;
vector<int> minHeap;
int size;
public:
/** initialize your data structure here. */
MedianFinder() {
size = 0;
}
void shiftUpMaxHeap(vector<int>& Heap, int pos) {
while (pos > 0) {
int parent = (pos - 1) / 2;
if (Heap[parent] < Heap[pos]) {
swap(Heap[parent], Heap[pos]);
}
pos = parent;
}
}
void shiftUpMinHeap(vector<int>& Heap, int pos) {
while (pos > 0) {
int parent = (pos - 1) / 2;
if (Heap[parent] > Heap[pos]) {
swap(Heap[parent], Heap[pos]);
}
pos = parent;
}
}
void shiftDownMaxHeap(vector<int>& heap, int pos, int n) {
while (2 * pos + 1 < n) {
int child = 2 * pos + 1;
if (child + 1 < n && heap[child + 1] > heap[child]) {
child ++;
}
if (heap[child] <= heap[pos]) {
break;
}
swap(heap[child], heap[pos]);
pos = child;
}
}
void shiftDownMinHeap(vector<int>& heap, int pos, int n) {
while (2 * pos + 1 < n) {
int child = 2 * pos + 1;
if (child + 1 < n && heap[child + 1] < heap[child]) {
child ++;
}
if (heap[child] >= heap[pos]) {
break;
}
swap(heap[child], heap[pos]);
pos = child;
}
}
void addNum(int num) {
size ++;
// maxHeap.push(num)
maxHeap.push_back(num);
shiftUpMaxHeap(maxHeap, maxHeap.size() - 1);
// minHeap.push(maxHeap.top())
minHeap.push_back(maxHeap[0]);
shiftUpMinHeap(minHeap, minHeap.size() - 1);
// maxHeap.pop()
swap(maxHeap[0], maxHeap.back());
maxHeap.pop_back();
shiftDownMaxHeap(maxHeap, 0, maxHeap.size());
if (size % 2 != 0) {
// maxHeap.push(minHeap.top())
maxHeap.push_back(minHeap[0]);
shiftUpMaxHeap(maxHeap, maxHeap.size() - 1);
// minHeap.pop();
swap(minHeap[0], minHeap.back());
minHeap.pop_back();
shiftDownMinHeap(minHeap, 0, minHeap.size());
}
}
double findMedian() {
if (size % 2 == 0) {
return (minHeap[0] + maxHeap[0]) / 2.0;
} else {
return maxHeap[0] / 1.0;
}
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
根据字符出现的频率排序
(堆)
本题使用堆,只是因为优先队列有对元素有自动排序的功能,其实因为要将所有的元素都要排序,所以和堆哈希表按数组中数字出现的频率排序没有区别,都是排序而已。只要当堆维护k个值而不是整个数组的时候,才可以发挥出堆和其他直接排序的方法的优势。
class Solution {
public:
struct cmp {
bool operator() (pair<char, int>& n1, pair<char, int>& n2) {
return n1.second < n2.second;
}
};
string frequencySort(string s) {
unordered_map<char, int> hash;
for (auto ch : s) {
hash[ch] ++;
}
priority_queue<pair<char, int>, vector<pair<char, int>>, cmp> q;
for (auto e : hash) {
q.push(e);
}
string ans;
while (!q.empty()) {
for (int i = 0; i < q.top().second; i ++) {
ans += q.top().first;
}
q.pop();
}
return ans;
}
};
(桶排序)
class Solution {
public:
string frequencySort(string s) {
// 记录数字出现的频率
unordered_map<char, int> hash;
for (auto ch : s) {
hash[ch] ++;
}
// 将数字按数字出现的频率大小放在数组中
// 为了防止不同数字有相同的出现频率,所以需要在vector中内嵌一个list
vector<list<char>> fre(s.size() + 1);
for (auto e : hash) {
fre[e.second].push_back(e.first);
}
// 将数组中的数字按频率的从高到低拿出
// 如果一个链表上有多个值需要拿出多个值
// 需要拿出i个相同的字符
string ans;
for (int i = fre.size() - 1; i >= 0; i --) {
int n = fre[i].size();
for (int j = 0; j < n; j ++) {
char ch = fre[i].front();
fre[i].pop_front();
for (int k = 0; k < i; k ++) {
ans += ch;
}
}
}
return ans;
}
};
(手写堆)
class Solution {
public:
void shiftUp(vector<pair<char, int>>& arr, int pos) {
while (pos > 0) {
int parent = (pos - 1) / 2;
if (arr[parent].second < arr[pos].second) {
swap(arr[parent], arr[pos]);
}
pos = parent;
}
}
void shiftDown(vector<pair<char, int>>& arr, int pos, int n) {
while (2 * pos + 1 < n) {
int child = 2 * pos + 1;
if (child + 1 < n && arr[child + 1].second > arr[child].second) {
child ++;
}
if (arr[child].second <= arr[pos].second) {
break;
}
swap(arr[child], arr[pos]);
pos = child;
}
}
string frequencySort(string s) {
unordered_map<char, int> hash;
for (auto ch : s) {
hash[ch] ++;
}
vector<pair<char, int>> arr;
for (auto e : hash) {
arr.push_back(e);
shiftUp(arr, arr.size() - 1);
}
string ans;
// 这里不能用迭代器,因为拿出字符的时候也需要按字符出现的频率拿出来
// 所以其中就会进行shiftDown(),所以arr就会进行一些调整
while (!arr.empty()) {
for (int i = 0; i < arr[0].second; i ++) {
ans += arr[0].first;
}
swap(arr[0], arr.back());
arr.pop_back();
shiftDown(arr, 0, arr.size());
}
return ans;
}
};
最接近原点的K个点
其实这道题目就是求出前K个小的元素的集合
(堆)
class Solution {
public:
typedef pair<int, int> PII;
struct cmp {
bool operator() (pair<PII, int>& a, pair<PII, int>& b) {
return a.second < b.second;
}
};
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
// 使用pair<pair<int, int>, int>
// 中间的pair<int,int>用来记录坐标,外面的pair<PII,int>用来记录坐标对应到原点的距离
priority_queue< pair<PII, int>, vector<pair<PII, int>>, cmp> q;
for (auto point : points) {
// 算出对应的坐标
int x = point[0];
int y = point[1];
int dis = x * x + y * y;
// 维护一个大小为k的大根堆
if (q.size() == k) {
if (dis < q.top().second) {
q.pop();
q.push({{x, y}, dis});
}
} else {
q.push({{x, y}, dis});
}
}
// 将堆中的元素放入ans中
vector<vector<int>> ans;
while (!q.empty()) {
auto top = q.top().first;
ans.push_back({top.first, top.second});
q.pop();
}
return ans;
}
};
(堆 简洁版)
class Solution {
public:
struct cmp {
bool operator() (vector<int>& a, vector<int>& b) {
return a[0] * a[0] + a[1] * a[1] < b[0] * b[0] + b[1] * b[1];
}
};
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
priority_queue<vector<int>, vector<vector<int>>, cmp> q;
// 将坐标整体作为元素放入大根堆中直接排序
for (auto point : points) {
if (q.size() == k) {
auto top = q.top();
if (top[0]*top[0] + top[1]*top[1]
> point[0]*point[0] + point[1]*point[1]) {
q.pop();
q.push(point);
}
} else {
q.push(point);
}
}
vector<vector<int>> ans;
while (!q.empty()) {
ans.push_back(q.top());
q.pop();
}
return ans;
}
};
(快排)
class Solution {
public:
typedef pair<int, int> PII;
// 从大到小的一趟快排
int quickSort(vector<pair<PII, int>>& arr, int l, int r) {
auto key = arr[l];
int keyi = l;
while (l < r) {
while (l < r && arr[r].second >= key.second) r --;
while (l < r && arr[l].second <= key.second) l ++;
swap(arr[l], arr[r]);
}
swap(arr[l], arr[keyi]);
return l;
}
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
// 将数组中对应的坐标放入arr数组中
vector<pair<PII, int>> arr;
for (auto point : points) {
int x = point[0];
int y = point[1];
arr.push_back({{x, y}, x * x + y * y});
}
// 二分出索引值为k-1的数字
int l = 0, r = points.size() - 1;
int target = k - 1;
while (l < r) {
int mid = quickSort(arr, l, r);
if (mid == target) break;
else if (mid > target) r = mid - 1;
else l = mid + 1;
}
// 将前k个小的元素放入ans
vector<vector<int>> ans;
for (int i = 0; i < k; i ++) {
auto top = arr[i].first;
ans.push_back({top.first, top.second});
}
return ans;
}
};
(手写堆)
class Solution {
public:
bool cmp(vector<int>& a, vector<int>& b) {
return a[0] * a[0] + a[1] * a[1] < b[0] * b[0] + b[1] * b[1];
}
void shiftUp(vector<vector<int>>& arr, int pos) {
while (pos > 0) {
int parent = (pos - 1) / 2;
if (cmp(arr[parent] , arr[pos])) {
swap(arr[parent], arr[pos]);
}
pos = parent;
}
}
void shiftDown(vector<vector<int>>& arr, int pos, int n) {
while (2 * pos + 1 < n) {
int child = 2 * pos + 1;
if (child + 1 < n && cmp(arr[child], arr[child + 1])) {
child ++;
}
if (cmp(arr[pos], arr[child])) {
swap(arr[child], arr[pos]);
pos = child;
} else {
break;
}
}
}
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
vector<vector<int>> arr;
// 维护一个大小为k+1的堆
for (auto point : points) {
if (arr.size() < k) {
arr.push_back(point);
shiftUp(arr, arr.size() - 1);
} else {
// push
arr.push_back(point);
shiftUp(arr, arr.size() - 1);
// pop()
swap(arr[0], arr.back());
arr.pop_back();
shiftDown(arr, 0, arr.size());
}
}
return arr;
}
};
(扫描线算法+堆优化)
天际线问题
扫描线算法:将每一条边的左右端点的坐标都放入一个数组中按左右端点的值来排序,然后用一个堆来维护所存在的端点的最大高度(这里因为priority_queue
不支持堆的中间删除的掺入元素,所以使用multiset
来代替)遇到左端点就将高度加入堆中,遇到右端点将高度从堆中删除。每当堆中的最大高度变大了,说明出现了关键点(红色的点都是关键点),就将该端点放入ans数组中。
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
vector<pair<int, int>> line;
for (auto& seg : buildings) {
line.push_back({seg[0], -seg[2]});
line.push_back({seg[1], seg[2]});
}
sort(line.begin(), line.end());
// //也可以直接使用multiset将左右的边缘坐标进行排序
// multiset<pair<int, int>> line;
// for (auto& seg : buildings) {
// line.insert({seg[0], -seg[2]});
// line.insert({seg[1], seg[2]});
// }
multiset<int> heap;
heap.insert(0);
int prev = 0, cur = 0;
vector<vector<int>> ans;
for (auto& coordinate : line) {
if (coordinate.second < 0) {
heap.insert(-coordinate.second);
} else {
heap.erase(heap.find(coordinate.second));
}
cur = *heap.rbegin();
if (cur != prev) {
ans.push_back({coordinate.first, cur});
prev = cur;
}
}
return ans;
}
};
堆排序总结
归并排序
合并两个有序数
(从前往后归并)
从前往后归并就是基础的归并排序的思想,但是不能再nums1
直接覆盖,因为这样就会影响nums1
后面的有效数字,所以需要将归并出来的数字放在一个额外的空间中,最后归并过程完成好之后,再将临时数组中的排好序的内容复制到nums1
中。
从前往后的办法会额外的开辟空间,这样空间复杂度就会变高。
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int start1 = 0, start2 = 0;
int index = 0;
vector<int> nums(m + n);
while (start1 < m && start2 < n) {
if (nums1[start1] <= nums2[start2]) nums[index ++] = nums1[start1 ++];
else nums[index ++] = nums2[start2 ++];
}
while (start1 < m) nums[index ++] = nums1[start1 ++];
while (start2 < n) nums[index ++] = nums2[start2 ++];
nums1.assign(nums.begin(), nums.end());
}
};
(从后往前归并)
因为从前往后的归并会覆盖nums1
中的数字,所以就可以从后往前的归并,这样既可以归并两个数组也可以不影响原数组中的内容而来进行归并。
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int index = m + n - 1;
n --;
m --;
while (m >= 0 && n >= 0) {
if (nums1[m] >= nums2[n]) nums1[index --] = nums1[m --];
else nums1[index --] = nums2[n --];
}
while (m >= 0) nums1[index --] = nums1[m --];
while (n >= 0) nums1[index --] = nums2[n --];
}
};
旋转数组
(开辟额外空间)
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
vector<int> newArr(n);
for (int i = 0; i < n; i ++) {
newArr[(i + k) % n] = nums[i];// 整体数组往后移动k位,并且循环数组
}
nums.assign(newArr.begin(), newArr.end());
}
};
- 时间复杂度O(N)
- 空间复杂度O(N)
(翻转数组)
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k %= n;
reverse(nums.begin(), nums.end());
reverse(nums.begin(), nums.begin() + k);
reverse(nums.begin() + k, nums.end());
}
};
*(数学)
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k %= n;
int cnt = 0; // 一共需要交换n个数字
for (int i = 0; cnt < n; i ++) {
int cur = i; // 从0位置开始交换
int prev = nums[cur];
do {
int next = (cur + k) % n;
swap(nums[next], prev); // 交换座位
cur = next; // 更新当前的位置
cnt ++; // 交换的数字个数+1
}while (i != cur); // 回到原点
}
}
};
数组中的逆序对
(暴力循环)NAC
class Solution {
public:
int reversePairs(vector<int>& nums) {
int ans = 0;
for (int i = 0; i < nums.size(); i ++) {
for (int j = i + 1; j < nums.size(); j ++) {
if (nums[i] > nums[j]) ans ++;
}
}
return ans;
}
};
(归并排序思想)
这是归并排序的过程,我们可以发现在归并的过程其实就是在让逆序对不断交换的过程,这也就是在消除逆序对的过程,所以就可以在两个数组的合并的时候,记录一下逆序对的数量即可。
合并过程一
合并过程二
归并数组结束,计算逆序对完毕
1.后数组有序,算出前数组中小元素的个数
class Solution {
public:
int mergeSort(vector<int>& nums, int l, int r) {
if (l >= r) return 0;
int mid = l + (r - l) / 2;
// 算出[l, mid]和[mid+1, r]中数组中逆序对对的个数
int cnt = mergeSort(nums, l, mid) + mergeSort(nums, mid + 1, r);
int start1 = l;
int start2 = mid + 1;
int index = 0;
vector<int> tmp(r - l + 1);
int ans = 0;
// 当后数组中的元素更小的时候,需要将前数组中比该元素小的元素个数加上
while (start1 <= mid && start2 <= r) {
if (nums[start1] <= nums[start2]) {
tmp[index ++] = nums[start1 ++];
} else {
tmp[index ++] = nums[start2 ++];
ans += mid + 1 - start1;
}
}
while (start1 <= mid) {
tmp[index ++] = nums[start1 ++];
}
// 和前数组算法不同的是,后数组算法中当前数组中没有数的时候,就不存在逆序对了
while (start2 <= r) {
tmp[index ++] = nums[start2 ++];
}
for (int i = 0, j = l; j <= r; j ++, i ++) {
nums[j] = tmp[i];
}
return cnt + ans;
}
int reversePairs(vector<int>& nums) {
return mergeSort(nums, 0, nums.size() - 1);
}
};
前数组有序,算出有数组中大元素的个数
class Solution {
public:
int mergeSort(vector<int>& nums, int l, int r) {
if (l >= r) return 0;
int mid = l + (r - l) / 2;
// 算出[l, mid]和[mid+1, r]中数组中逆序对对的个数
int cnt = mergeSort(nums, l, mid) + mergeSort(nums, mid + 1, r);
int start1 = l;
int start2 = mid + 1;
int index = 0;
vector<int> tmp(r - l + 1);
int ans = 0;
// 当前数组中的元素更小的时候,需要将后数组中比该元素大的元素个数加上
while (start1 <= mid && start2 <= r) {
if (nums[start1] <= nums[start2]) {
ans += start2 - (mid + 1);
tmp[index ++] = nums[start1 ++];
} else {
tmp[index ++] = nums[start2 ++];
}
}
// 当后数组中没有数的时候,后数组中所有剩下的元素都比后数组中原来的数要大
// 所以全部都要算上
while (start1 <= mid) {
ans += r - mid;
tmp[index ++] = nums[start1 ++];
}
while (start2 <= r) {
tmp[index ++] = nums[start2 ++];
}
for (int i = 0, j = l; j <= r; j ++, i ++) {
nums[j] = tmp[i];
}
return cnt + ans;
}
int reversePairs(vector<int>& nums) {
// 归并排序中需要交换元素的时候就是在还原逆序对
return mergeSort(nums, 0, nums.size() - 1);
}
};
计算右侧小于当前元素的个数
(二分)TLE
class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
vector<int> ans(nums.size(), 0);
vector<int> sortNums;
for (int i = nums.size() - 1; i >= 0; i --) {
auto it = lower_bound(sortNums.begin(), sortNums.end(), nums[i]);
ans[i] = it - sortNums.begin();
sortNums.insert(it, nums[i]);
}
return ans;
}
};
(归并排序思想)
前数组有序算法,算出后数组中的小元素的个数
class Solution {
public:
using PII = pair<int, int>;
void mergeSort(int l, int r, vector<int>& ans, vector<PII>& indexNums) {
if (l >= r) return ;
int mid = l + (r - l) / 2;
mergeSort(l, mid, ans, indexNums);
mergeSort(mid + 1, r, ans, indexNums);
vector<PII> tmp(r - l + 1);
int start1 = l;
int start2 = mid + 1;
int index = 0;
// 使用前数组有序的好处在于可以使用O(1)的时间就可以求出一个元素对应的逆序对
while (start1 <= mid && start2 <= r) {
if (indexNums[start1].first <= indexNums[start2].first) {
ans[indexNums[start1].second] += start2 - (mid + 1);
tmp[index ++] = indexNums[start1 ++];
} else {
tmp[index ++] = indexNums[start2 ++];
}
}
while (start1 <= mid) {
ans[indexNums[start1].second] += r - mid;
tmp[index ++] = indexNums[start1 ++];
}
while (start2 <= r) {
tmp[index ++] = indexNums[start2 ++];
}
for (int i = 0, j = l; j <= r; j ++, i ++) {
indexNums[j] = tmp[i];
}
}
vector<int> countSmaller(vector<int>& nums) {
vector<int> ans(nums.size(), 0);
vector<PII> indexNums;
for (int i = 0; i < nums.size(); i ++) {
indexNums.push_back({nums[i], i});
}
mergeSort(0, nums.size() - 1, ans, indexNums);
return ans;
}
};
后数组有序算法,算出前数组中大元素的个数(TEL)
class Solution {
public:
using PII = pair<int, int>;
void mergeSort(int l, int r, vector<int>& ans, vector<PII>& indexNums) {
if (l >= r) return ;
int mid = l + (r - l) / 2;
mergeSort(l, mid, ans, indexNums);
mergeSort(mid + 1, r, ans, indexNums);
vector<PII> tmp(r - l + 1);
int start1 = l;
int start2 = mid + 1;
int index = 0;
while (start1 <= mid && start2 <= r) {
if (indexNums[start1].first <= indexNums[start2].first) {
tmp[index ++] = indexNums[start1 ++];
} else {
// 将前数组中的所有大数都一个一个统计一遍
for (int i = start1; i <= mid; i ++) {
ans[indexNums[i].second] ++;
}
tmp[index ++] = indexNums[start2 ++];
}
}
while (start1 <= mid) {
tmp[index ++] = indexNums[start1 ++];
}
while (start2 <= r) {
tmp[index ++] = indexNums[start2 ++];
}
for (int i = 0, j = l; j <= r; j ++, i ++) {
indexNums[j] = tmp[i];
}
}
vector<int> countSmaller(vector<int>& nums) {
vector<int> ans(nums.size(), 0);
vector<PII> indexNums;
for (int i = 0; i < nums.size(); i ++) {
indexNums.push_back({nums[i], i});
}
mergeSort(0, nums.size() - 1, ans, indexNums);
return ans;
}
};
逆序对算法总结
总结:在计算逆序对个数的时候,优先采用后数组有序算法,因为可以少写一个判断等式。在计算一个元素对应后面元素的逆序对个数的时候,优先采用前数组有序算法,因为该算法本身就可以使用O(1)的时候算出一个元素的逆序对,而后数组有序算法,只能算出前面逆序对的个数,如果想要算出逆序对的个数时就必须要按当前的元素作为小元素,遍历一遍前数组中对应的大元素,然后一个一个计算+1。
快速排序
数组中第K个最大的元素
(快速排序思想)
class Solution {
public:
int quickSort(vector<int>& nums, int l, int r) {
int pivot = nums[l];
int lt = l;
for (int i = l + 1; i <= r; i ++) {
if (nums[i] < pivot) {
lt ++;
swap(nums[lt], nums[i]);
}
}
swap(nums[lt], nums[l]);
return lt;
}
int findKthLargest(vector<int>& nums, int k) {
int target = nums.size() - k;
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = quickSort(nums, l, r);
if (mid == target) return nums[target];
else if (mid > target) r = mid - 1;
else l = mid + 1;
}
return nums[l];
}
};
(堆排序思想)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> q;
for (int i = 0; i < k; i ++) {
q.push(nums[i]);
}
for (int i = k; i < nums.size(); i ++) {
if (q.size() == k) {
if (q.top() < nums[i]) {
q.pop();
q.push(nums[i]);
}
} else {
q.push(nums[i]);
}
}
return q.top();
}
};
(桶排序思想)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 求出桶的大小的最大上限
int maxSize = 0;
for (int num : nums) {
maxSize = max(maxSize, abs(num));// 需要保证maxSize一定是大于0的
}
// 桶中的下标为元素的大小
vector<list<int>> bucket(maxSize * 2 + 1);// 因为可能有负数,所以所有的数都+maxSize
// 所以就需要准备2*maxSize+1个空间
for (int num : nums) {
bucket[maxSize + num].push_back(num);
}
// 从后往前遍历
for (int i = maxSize * 2; i >= 0; i --) {
while (!bucket[i].empty()) {
k --;
if (k == 0) {
return i - maxSize;// 最后第k个对应的元素还需要-maxSize
}
bucket[i].pop_back();
}
}
return 0;
}
};
删除排序数组中的重复元素
(双指针1)
将数组中的数字遍历一遍,如果数组中遍历的元素和nums[j]
相同就直接跳过,如果不相同就将该数字移至j+1
的位置,由于nums[j]
和nums[0]
相同,所以必须使用nums[++ j] = nums[i]
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int j = 0;
for (int i = 0; i < nums.size(); i ++) {
if (nums[i] != nums[j]) {
nums[++ j] = nums[i];
}
}
return j + 1;
}
};
或者可以多加一个判断j < 1
的时候,也就是是无论数组中的数字,第一个数字都是必须要选的
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int j = 0;
for (int i = 0; i < nums.size(); i ++) {
if (j < 1 || nums[i] != nums[j - 1]) {
nums[j ++] = nums[i];
}
}
return j;
}
};
(双指针2)
这种方案是利用双指针解决连续相同的数字的通解。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int k = 0;
for (int i = 0; i < nums.size(); i ++) {
nums[k ++] = nums[i];
int j = i;
while (j < nums.size() && nums[j] == nums[i]) j ++;
i = j - 1;
}
return k;
}
};
删除排序数组中的重复项ll
(双指针1)
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int j = 0;
for (int i = 0; i < nums.size(); i ++) {
if (j < 2 || nums[j - 2] != nums[i]) {
nums[j ++] = nums[i];
}
}
return j;
}
};
(双指针2)
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n = nums.size();
int k = 0;
for (int i = 0; i < n; i ++) {
nums[k ++] = nums[i];
int j = i;
while (j < n && nums[i] == nums[j]) j ++;
if (j - i >= 2) {// 如果不相同的数字超过了2个,数组前就在加一个nums[i]
nums[k ++] = nums[i];
}
i = j - 1;
}
return k;
}
};
*删除排序数组中的重复项(扩展)->通用模板
题目:每一个元素最多可以出现x次
(双指针1)
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int x = 3;// 每个元素最多出现x次
int j = 0;
for (int i = 0; i < nums.size(); i ++) {
if (j < x || nums[j - x] != nums[i]) {
nums[j ++] = nums[i];
}
}
return j;
}
};
(双指针2)
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int x = 3;// 每个元素最多出现x次
int n = nums.size();
int k = 0;
for (int i = 0; i < n; i ++) {
nums[k ++] = nums[i];
int j = i;
while (j < n && nums[i] == nums[j]) j ++;
if (j - i >= x) {
for (int l = 0; l < x - 1; l ++)// 需要在新添x-1个nums[i]
nums[k ++] = nums[i];
}
i = j - 1;
}
return k;
}
};
颜色分类
(sort)
直接对数组进行排序,sort
、快排、堆排序都可以
*(快速排序的partition)
class Solution {
public:
void sortColors(vector<int>& nums) {
if (nums.size() <= 1) return ;
// [0, zero] == 0
// (zero, i) == 1;
// [two, nums.size() - 1] == 2
int zero = -1, two = nums.size();
int index = 0;
while (index < two) {
if (nums[index] < 1) {
zero ++;
swap(nums[zero], nums[index]);
index ++;
} else if (nums[index] == 1) {
index ++;
} else {
two --;
swap(nums[two], nums[index]);
}
}
}
};
(单指针)
class Solution {
public:
void sortColors(vector<int>& nums) {
int index = 0;
for (int& num : nums) {
if (num == 0) {
swap(nums[index ++], num);
}
}
for (int& num : nums) {
if (num == 1) {
swap(nums[index ++], num);
}
}
for (int& num : nums) {
if (num == 2) {
swap(nums[index ++], num);
}
}
}
};
根据字符出现的频率排序
(快速排序)
class Solution {
public:
using PII = pair<int, int>;
void QuickSort(vector<PII>& nums, int l, int r) {
if (l >= r) return ;
int left = l;
int right = r;
auto pivot = nums[l];
while (l < r) {
while (l < r && nums[r].second <= pivot.second) r --;
nums[l] = nums[r];
while (l < r && nums[l].second >= pivot.second) l ++;
nums[r] = nums[l];
}
nums[l] = pivot;
QuickSort(nums, left, l - 1);
QuickSort(nums, l + 1, right);
}
string frequencySort(string s) {
// 记录字符出现的频率
unordered_map<char, int> hash;
for (char ch : s) {
hash[ch] ++;
}
// 将哈希表中的数据放入nums数组中,然后使用排序进行按字符频率降序排列
vector<PII> nums(hash.begin(), hash.end());
QuickSort(nums, 0, nums.size() - 1);
// 将字符按降序排列拼接起来
string ans;
for (auto& ch : nums) {
while (ch.second --) {
ans += ch.first;
}
}
return ans;
}
};
(堆排序)
class Solution {
public:
using PII = pair<int, int>;
struct cmp {
bool operator() (PII& a, PII& b) {
return a.second < b.second;
}
};
string frequencySort(string s) {
unordered_map<char, int> hash;
for (auto ch : s) {
hash[ch] ++;
}
priority_queue<PII, vector<PII>, cmp> q;
for (auto e : hash) {
q.push(e);
}
string ans;
while (!q.empty()) {
for (int i = 0; i < q.top().second; i ++) {
ans += q.top().first;
}
q.pop();
}
return ans;
}
};
(桶排序)
提醒:
1.在桶排序中**vector<list<int>>
**是防止相同频率的数字出现。
2.在最后频率倒数的时候需要从频率的最后一个开始,而不是从bucket.size()
,因为bucket.size()
比s.size()
多1。
3.在取出数字的时候,需要取出下标i
个元素,因为下标就是字符出现的频率,而不能直接取出一个数字,所以在判断!bucket.empty()
的时候,需要使用while
循环判断,而不能使用if
只判断一次。
class Solution {
public:
string frequencySort(string s) {
unordered_map<char, int> hash;
for (auto ch : s) {
hash[ch] ++;
}
vector<list<int>> bucket(s.size() + 1);
for (auto e : hash) {
bucket[e.second].push_back(e.first);
}
string ans;
for (int i = s.size(); i >= 0; i --) {
while (!bucket[i].empty()) {
for (int k = 0; k < i; k ++) ans += bucket[i].back();
bucket[i].pop_back();
}
}
return ans;
}
};
多数元素
(直接sort,去nums[n/2])
直接sort
数组,然后去中间数即可。
(变量计数)
class Solution {
public:
int majorityElement(vector<int>& nums) {
// 用一个count计数,因为多数元素的个数>n/2,所以最后使得count>0的数就是多数元素
int count = 0;
int prev = 0;
for (int i = 0; i < nums.size(); i ++) {
if (count == 0) {
count ++;
prev = nums[i];
} else {
if (prev == nums[i]) count ++;
else count --;
}
}
return prev;
}
};
(哈希表)
class Solution {
public:
int majorityElement(vector<int>& nums) {
// 用哈希表计数
int n = nums.size();
unordered_map<int, int> hash;
for (int num : nums) {
hash[num] ++;
if (hash[num] > n / 2) {
return num;
}
}
return 0;
}
};
(快速排序之分组思想)
class Solution {
public:
int ans = 0;
// 利用快速排序的分组partition思想
void QuickSort(vector<int>& nums, int l, int r) {
if (l >= r) return ;
int pivot = nums[l];
int lt = l, gt = r + 1;
int index = l + 1;
while (index < gt) {
if (nums[index] < pivot) {
lt ++;
swap(nums[lt], nums[index]);
index ++;
} else if (nums[index] == pivot) {
index ++;
} else {
gt --;
swap(nums[gt], nums[index]);
}
}
swap(nums[lt], nums[l]);
// [left, right]是nums[l]这一组相同的数字
int left = lt;
int right = gt - 1;
if (right - left + 1 > nums.size() / 2) {// 如果该元素的范围>n/2就直接记录ans并返回
ans = nums[left];
return ;
}
QuickSort(nums, l, lt - 1);
QuickSort(nums, gt, r);
}
int majorityElement(vector<int>& nums) {
// 当数组中只有一个元素的时候,无法排序所以需要特判
if (nums.size() == 1) return nums[0];
// 利用快速排序的分组partition思想
QuickSort(nums, 0, nums.size() - 1);
return ans;
}
};
快速排序总结
总结:在使用快速排序可以解决一下问题:
1.快排在使用三数取中加上挖洞法/指针对撞法/分组思想其中任意一个模板都可以使得数组得到**O(NlogN)**的时间复杂度。
1.使用所有的快排模板加上二分思想就可以解决TopK问题,也就是找出第k大(小)的数。
2.使用快排的分组思想,可以只进行一次就可以使得数组分成三组[left, target]
、[target, target]
、[target, right]
。这三组数据,其中中间一段都是target
也就是在partition
中选出的pivot
。
桶排序
缺失的第一个正数
(哈希表)
使用哈希表统计数组中的所有数字,然后再1~~N
中找哈希表中没有的数字。
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
unordered_set<int> hash;
for (int num : nums) {
hash.insert(num);
}
for (int i = 1; i <= nums.size(); i ++) {
if (!hash.count(i)) return i;
}
return nums.size() + 1;
}
};
- 时间复杂度O(N)
- 空间复杂度O(N)
(数组做哈希表1)
首先需要明确一点:缺失的第一个正数一定在1~nums.size()
之间,因为如果数组中的数全部都为负数,则第一个正数就是1。如果数组中的数全部都> nums.size()
的话,一个正数就也是1。所以数组中第一个缺失的正数一定在1~nums.size()
,最极端的例子就是数组中的数正好就是1~nums.size()
,那么久返回nums.size() + 1
。
核心思想就是如果处在1~N
中的数,那么nums[i]
应该处在下标i + 1
的位置上,如果只要遇到nums[i] != i + 1
,就让nums[i]
交换到nums[nums[i] - 1]
的位置上。
不过如果nums[i]<=0
因为是是负数没来就没有自己对应的位置多以就不用换了,如果nums[i] > nums.size()
,因为> nums.size()
的数也没有自己对应的位置所以也不用换了,最后如果nums[i] == nums[num[i] - 1]
的话,因为两个位置上的相同,但是只有一个位置,所以就不用交换了以避免死循环。
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
for (int i = 0; i < nums.size(); i ++) {
while (nums[i] != i + 1) {
if (nums[i] <= 0 || nums[i] > nums.size()
|| nums[i] == nums[nums[i] - 1])
break;
swap(nums[i], nums[nums[i] - 1]);
}
}
for (int i = 0; i < nums.size(); i ++){
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.size() + 1;
}
};
- 时间复杂度O(N)
- 空间复杂度O(1)
(数组作哈希表2)
上面说了使用数组对号入座的核心就是nums[i] = i + 1
,但是如果swap
消耗的成本大的话,可以将nums[i]
对应的位置上变成负数,如果这样因为缺失一个在1~nums.size()
中的数,所以一定会剩下一个正数。而这个正数的下标+1就是对应的缺失的正数。
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for (int& num : nums) {
if (num <= 0) num = n + 1;
}
for (int i = 0; i < nums.size(); i ++) {
if (abs(nums[i]) <= n)
nums[abs(nums[i]) - 1] = -abs(nums[abs(nums[i]) - 1]);
}
for (int i = 0; i < nums.size(); i ++) {
if (nums[i] > 0) {
return i + 1;
}
}
return nums.size() + 1;
}
};
- 时间复杂度O(N)
- 空间复杂度O(1)
丢失的数字
(哈希表)
数组做桶
使用哈希表记录数字出现过的数字,然后在0~nums.size() -1
中找未出现过的数字。
class Solution {
public:
int missingNumber(vector<int>& nums) {
vector<int> tmp(nums.size() + 1);
for (int num : nums) {
tmp[num] ++;
}
for (int i = 0; i < tmp.size(); i ++) {
if (tmp[i] == 0) return i;
}
return tmp.size();
}
};
- 时间复杂度O(N)
- 空间复杂度O(N)
同上解法,使用哈希表
class Solution {
public:
int missingNumber(vector<int>& nums) {
unordered_set<int> hash;
for (int num : nums) {
hash.insert(num);
}
for (int i = 0; i < nums.size(); i ++) {
if (!hash.count(i)) return i;
}
return nums.size();
}
};
- 时间复杂度O(N)
- 空间复杂度O(N)
(二分)
class Solution {
public:
int missingNumber(vector<int>& nums) {
// 出现在缺失数字之前的数字>num的数字应该等于num+1
// 出现在缺失数字之后的数字应该小于num+1
int l = 0, r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
int cnt = 0;
for (int n : nums) {// 统计<=mid数字的个数
if (n <= mid) cnt ++;
}
if (cnt < mid + 1) r = mid;
else l = mid + 1;
}
return l;
}
};
- 时间复杂度O(NlogN)
- 空间复杂度O(1)
(数组做哈希表1)
本题和缺失的第一个正数的区别在于:缺失的第一个正数这题中数组中的存放的数应该与i + 1
下标意义对应,最后找出没有对应的数。但是这题需要求出第一个缺失的数字,而且数字必须都是[0, nums.size() - 1]
中的数。所以数字中多包含了一个0,由此需要在数组中多扩展一个空间,这样才可以使得缺失数字后面的数字都可以让nums[i]
与i
对应上。
class Solution {
public:
int missingNumber(vector<int>& nums) {
nums.push_back(-1);// 给数组扩容1个位置
// 让数组中的nums[i]对应着下标i
for (int i = 0; i < nums.size(); i ++) {
while (nums[i] != i) {
if (nums[i] < 0) break;
swap(nums[i], nums[nums[i]]);
}
}
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != i) return i;
}
return nums.size() + 1;
}
};
- 时间复杂度O(N)
- 空间复杂度O(1)
数组做哈希表2
class Solution {
public:
int missingNumber(vector<int>& nums) {
nums.push_back(nums[0]);
int n = nums.size();
for (int i = 0; i < nums.size(); i ++) {
int index = nums[i] % n;
nums[index] += n;
}
for (int i = 0; i < nums.size(); i ++) {
if (nums[i] < n) return i;
}
return nums.size() - 1;
}
};
(位运算)
因为已知了数字的范围在0~nums.size()
,所以可以使用异或运算将所有数字相同的数字都抵消掉,这样nums[i]
和i
对应起来的数字就会被抵消掉,最终剩下的数字就是丢失的数字。
class Solution {
public:
int missingNumber(vector<int>& nums) {
int missing = nums.size();
for (int i = 0; i < nums.size(); i ++) {
missing ^= i ^ nums[i];// 等价于missing = missing ^ i ^ nums[i]
}
return missing;
}
};
- 时间复杂度O(N)
- 空间复杂度O(1)
找到数组中消失的数字
(哈希表)
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
unordered_set<int> hash;
for (int num : nums) {
hash.insert(num);
}
vector<int> ans;
for (int i = 1; i <= nums.size(); i ++) {
if (!hash.count(i)) ans.push_back(i);
}
return ans;
}
};
(数组做哈希表1)
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
for (int i = 0; i < nums.size(); i ++) {
while (nums[i] != i + 1) {
if (nums[i] == nums[nums[i] - 1]) break;
swap(nums[i], nums[nums[i] - 1]);
}
}
vector<int> ans;
for (int i = 0; i < nums.size(); i ++) {
if (nums[i] != i + 1) ans.push_back(i + 1);
}
return ans;
}
};
(数组做哈希表2)
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
for (int i = 0; i < nums.size(); i ++) {
int index = abs(nums[i]) - 1;
if (nums[index] > 0) {
nums[index] *= -1;
}
}
vector<int> ans;
for (int i = 0; i < nums.size(); i ++){
if (nums[i] > 0) {
ans.push_back(i + 1);
}
}
return ans;
}
};
寻找重复数
(哈希表)
class Solution {
public:
int findDuplicate(vector<int>& nums) {
unordered_set<int> hash;
for (int num : nums) {
if (hash.count(num)) return num;
hash.insert(num);
}
return 0;
}
};
- 时间复杂度O(N)
- 空间复杂度O(N)
(数组做哈希表1)
class Solution {
public:
int findDuplicate(vector<int>& nums) {
for (int i = 0; i < nums.size(); i ++) {
// 如果数字nums[i]没有对应i+1,就让nums[i]到下标为nums[i]-1的位置上
while (nums[i] - 1 != i) {
if (nums[i] == nums[nums[i] - 1]) return nums[i];
swap(nums[i], nums[nums[i] - 1]);
}
}
return 0;
}
};
- 时间复杂度O(N)
- 空间复杂度O(1)
(数组做哈希表2)
class Solution {
public:
int findDuplicate(vector<int>& nums) {
for (int i = 0; i < nums.size(); i ++) {
int index = abs(nums[i]) - 1;
if (nums[index] < 0) return abs(nums[i]);
nums[index] *= -1;
}
return 0;
}
};
(二分)
因为数字一定在[1, nums.size()]
之间,并且只会出现一个重复的数字,所以可以使用二分,利用二段性,也就是在重复数字出现以前>num
数字的个数== num
,但是出现重复数字以后>num
数字的个数== mid + 1
。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
// 找到第一个重复数字,也就是找到第一个>=num数的个数超过num+1的数字
int l = 1, r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
int cnt = 0;
for (int n : nums) {// 统计>=mid数字的个数
if (mid >= n) cnt ++;
}
if (cnt >= mid + 1) r = mid;
else l = mid + 1;
}
return l;
}
};
- 时间复杂度O(NlogN)
- 空间复杂度O(1)
(快慢指针)
如果出现重复的数字就会形成一个环,而环的入口点就是重复的数字,因为本题中只出现一个重复的数字,所以换的入口点是唯一的。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int fast = 0, slow = 0;
while (true) {
fast = nums[nums[fast]];
slow = nums[slow];
if (fast == slow) break;
}
fast = 0;
while (fast != slow) {
fast = nums[fast];
slow = nums[slow];
}
return fast;
}
};
- 时间复杂度O(N)
- 空间复杂度O(1)
数组中的重复数据
(哈希表)
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
vector<int> ans;
unordered_set<int> hash;
for (int num : nums) {
if (hash.count(num)) ans.push_back(num);
hash.insert(num);
}
return ans;
}
};
(数组做哈希表2)
本题使用数组做哈希表的原理和缺失的第一个正数的原理相似。
在缺失第一个正数中,数组做哈希表是让数组中的数字对应着数组中的下标,所以每一个正数都可以对应这一个下标,最后从前往后遍历,如果没有对应的第一个正数就只缺失的第一个正数(其中对应数字可以使对应数字变成负数)。
本题是出现了重复的元素,也可以使用这种对应方法,而这次不是没有对应的数组是答案,而是对应了两次的数字是答案。
注意:因为对应的过程是将数字变成负数的形式,所以如果前面数字对应着后面的下标的话,那后面对应下标上的数字就会变为负数,因而在每一次取出数字的时候必须带上绝对值符号。
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
vector<int> ans;
for (int i = 0; i < nums.size(); i ++) {
if (nums[abs(nums[i]) - 1] < 0) ans.push_back(abs(nums[i]));// 对应了两次。
nums[abs(nums[i]) - 1] *= -1;
}
return ans;
}
};
桶排序总结
总结:一个数组中的找出消失的数字或者重复的数字题型