减轻复习压力,一篇只有十题左右
- 1.反转链表II
- 2.LRU缓存
- 3.合并区间
- 4.快速排序
- 5.数字中的第k个最大元素
- 6.归并排序
- 7.每种字符至少取k个
- 8.螺旋矩阵II
- 9.旋转图像
- 10.删除数组中重复的元素II
1.反转链表II
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
假如left == 1,需要反转的部分从链表的第一个节点就开始,那么翻转后链表的头结点将发生变化,使用dummy可以特殊处理这种情况。
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* p0 = dummy;
for(int i = 1; i < left; i ++) p0 = p0->next;
ListNode* pre = nullptr;
ListNode* cur = p0->next;
for(int i = 0; i < right - left + 1; i ++){
ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
p0->next->next = cur;
p0->next = pre;
return dummy->next;
}
};
2.LRU缓存
请你设计并实现一个满足最近最少使用缓存约束的数据结构,LRUCache类:
- LRUCache(int capacity)以正整数作为容量capacity初始化LRU缓存
- int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1
- void put(int key,int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该逐出最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
题目中提到key-value,所以涉及哈希表
题目的难点在于维护出入,首先想到栈,但是题目要求get和put还必须是O(1),所以栈、队列都不行,要用链表
链表+哈希,用双向链表
为什么是双向链表,因为双向链表在删除的时候是O(1),而不需要前驱节点
//双向链表结构体的实现
class Node{
public:
int key, val;
Node* next;
Node* pre;
Node(int k, int v){
key = k;
val = v;
}
};
class DoubleList{
private:
Node* head;
Node* tail;
int size;//链表的元素数
public:
DoubleList(){
//初始化
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->pre = head;
size = 0;
}
//在链表尾部添加节点x,O(1)
void addLast(Node* x){
x->pre = tail->pre;
x->next = tail;
tail->pre->next = x;
tail->pre = x;
size ++;
}
//删除链表中的x节点,(x一定存在)
void remove(Node* x){
x->pre->next = x->next;
x->next->pre = x->pre;
size --;
}
//删除链表第一个节点,并返回
Node* removeFirst(){
if(head->next == tail) return nullptr;
Node* first = head->next;
remove(first);
return first;
}
int getsize(){
return size;
}
};
class LRUCache {
private:
// key -> Node(key, val)
unordered_map<int, Node*> map;
// Node(k1, v1) <-> Node(k2, v2)...
DoubleList cache;
// 最大容量
int cap;
public:
LRUCache(int capacity) {
cap = capacity;
}
// 将某个 key 提升为最近使用的
void makeRevently(int key){
// 先拿到这个key对应的指针
Node* x = map[key];
// 在原本的位置删除它
cache.remove(x);
// 最近使用的加入到队尾
cache.addLast(x);
}
// 添加最近使用的元素
void addRecently(int key, int val){
Node* x = new Node(key, val);
cache.addLast(x);// 将最近使用的添加到链表中
map[key] = x;// 在map中添加key的映射
}
// 删除某一个key
void deleteKey(int key){
Node* x = map[key];
cache.remove(x);
map.erase(key);
}
//删除最久未用的元素
void removeLeastRecently(){
// 链表的第一个元素就是最久没用的
Node* deletedNode = cache.removeFirst();
// 先得到节点的key,然后从map里删除
int deletedKey = deletedNode->key;
map.erase(deletedKey);
}
int get(int key) {
if(!map.count(key)){
return -1;
}
makeRevently(key);
return map[key]->val;
}
void put(int key, int val) {
if(map.count(key)){
deleteKey(key);
addRecently(key, val);
return;
}
if(cap == cache.getsize()){
removeLeastRecently();
}
addRecently(key, val);
}
};
3.合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
注意他的for是怎么遍历的
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
//按照start排序
sort(intervals.begin(), intervals.end(),[](vector<int> a, vector<int> b){return a[0] < b[0];});
vector<vector<int>> res;
//把第一个元素push进去
res.push_back(intervals[0]);
for(int i = 1; i < intervals.size(); i ++){
//区间重叠
if(res.back()[1] >= intervals[i][0]){
res.back()[1] = max(res.back()[1], intervals[i][1]);
}
//区间不重叠
else{
res.push_back(intervals[i]);
}
}
return res;
}
};
4.快速排序
快排就是分治,给一个基准值,左边都小于这个基准值,右边都大于这个基准值
分别排序左右,合并就是有序数组了
注意mid = nums[(l + r) / 2],而不是mid = (l + r) / 2,不要用索引直接用值
class Solution {
public:
void quick_sort(vector<int>& nums, int l, int r){
if(l >= r) return;
int left = l - 1, right = r + 1, mid = nums[(l + r) / 2];
while(left < right){
do left ++; while(nums[left] < mid);
do right--;while(nums[right] > mid);
if(left < right) swap(nums[left], nums[right]);
}
quick_sort(nums, l, right);
quick_sort(nums, right + 1, r);
}
vector<int> sortArray(vector<int>& nums) {
quick_sort(nums, 0, nums.size() - 1);
return nums;
}
};
5.数字中的第k个最大元素
真的很想sort,但是这道题考频这么高,不可能让你调用库函数…
如果对原数组排序,再返回倒数第k个位置,这样复杂度是O(nlogn)
其实可以更快
在快排中,每次经过划分后,一定可以确定一个元素的最终位置,即x的最终位置是q,并且a[l…q - 1]中的每个元素小于等于a[q],且a[q]小于等于a[q + 1…r]中的每个元素,所以只要某次划分的q为倒数第k个下标的时候,我们就找到了答案,只需关注这点。
class Solution {
public:
int quickselect(vector<int> &nums, int l, int r, int k) {
if (l == r)
return nums[k];
int mid = nums[(l + r)/2], i = l - 1, j = r + 1;
while (i < j) {
do i++; while (nums[i] < mid);
do j--; while (nums[j] > mid);
if (i < j)
swap(nums[i], nums[j]);
}
if (k <= j) return quickselect(nums, l, j, k);
else return quickselect(nums, j + 1, r, k);
}
int findKthLargest(vector<int> &nums, int k) {
int n = nums.size();
return quickselect(nums, 0, n - 1, n - k);
}
};
6.归并排序
如果说快排是前序,那么归并就是后序,一个是先计算再递归,一个是先递归再计算
两个都属于分治算法
void merge_sort(vector<int>& nums, int lo, int hi){
if(lo >= hi) return;
int mid = (lo + hi) >> 1;
merge_sort(nums, lo, mid);
merge_sort(nums, mid + 1, hi);
vector<int> tmp(hi - lo + 1); // 创建一个新的 tmp 数组
int k = 0;
int i = lo, j = mid + 1;
while(i <= mid && j <= hi){
if(nums[i] > nums[j]){
tmp[k++] = nums[j++];
}
else{
tmp[k++] = nums[i++];
}
}
while(i <= mid) tmp[k++] = nums[i++];
while(j <= hi) tmp[k++] = nums[j++];
for(int i = lo, j = 0; i <= hi && j < k; i ++, j ++){
nums[i] = tmp[j];
}
}
7.每种字符至少取k个
给你一个由字符 ‘a’、‘b’、‘c’ 组成的字符串 s 和一个非负整数 k 。每分钟,你可以选择取走 s 最左侧 还是 最右侧 的那个字符。
你必须取走每种字符 至少 k 个,返回需要的 最少 分钟数;如果无法取到,则返回 -1 。
两边不好想,用滑动窗口想中间
怎么看外面字符呢,用一个tar数组,初始化为-k,for遍历++,tar中存的就是比k多的数量
滑动窗口内的数字,和tar里面留的数字比较,滑动窗口的数字小于tar中的,说明两侧这个字母的数量>k,滑动窗口的数字大于tar中的,说明两侧这个字母小于k
什么时候缩小窗口:因为当窗口中某个字符的数量 超过 tar 时,就意味着在剩余的字符串中,该字符的数量将 少于 k,这不符合题目要求。tar[i] 表示的是在可以移除的字符中,每种字符最多能移除的数量(即总数量减去 k)。因此,在滑动窗口中,我们需要确保窗口中每种字符的数量 不超过 tar[i]。
class Solution {
public:
int takeCharacters(string s, int k) {
vector<int> tar(3, -k); // 初始化目标数组为-k
for (char c : s) tar[c - 'a']++; // 计数数组
// 如果任意字符计数小于0,则无法形成有效子串
if (tar[0] < 0 || tar[1] < 0 || tar[2] < 0) return -1;
// 如果所有字符恰好达到k次,返回整个字符串长度
if (tar[0] == 0 && tar[1] == 0 && tar[2] == 0) return s.length();
int l = 0, r = 0, res = 0;
vector<int> cnt(3, 0); // 计数当前窗口中各字符的数量
while (r < s.length()) {
cnt[s[r++] - 'a']++; // 扩展窗口右边界
// 当窗口中任一字符数量超过tar时,收缩窗口左边界
while (cnt[0] > tar[0] || cnt[1] > tar[1] || cnt[2] > tar[2]) {
cnt[s[l++] - 'a']--;
}
// 更新最大子串长度
res = max(res, r - l);
}
// 返回除去最短有效子串后的剩余长度
return s.length() - res;
}
};
8.螺旋矩阵II
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
四个方向循环的表达式 dirIdx = (dirIdx + 1) % 4; 是一种非常高效且常用的方式来实现这种循环控制:
- dirIdx + 1:这部分是将当前的方向索引增加1,以便移动到下一个方向。
- 取模运算符 % 用来实现循环效果。因为有四个方向,所以取模4。这意味着当 dirIdx 从3增加1变为4时,4 % 4 的结果是0,从而将方向索引重置为0(即向右方向),形成一个循环。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.empty()) return {}; // 处理空矩阵的情况
int n = matrix.size();
int m = matrix[0].size();
vector<int> res;
vector<vector<int>> direction = {
{0, 1}, // 向右移动
{1, 0}, // 向下移动
{0, -1}, // 向左移动
{-1, 0} // 向上移动
};
vector<vector<bool>> seen(n, vector<bool>(m, false));
int i = 0, j = 0, dirIdx = 0; // dirIdx 用来控制方向
for (int k = 0; k < m * n; k++) {
res.push_back(matrix[i][j]);
seen[i][j] = true;
int nexti = i + direction[dirIdx][0];
int nextj = j + direction[dirIdx][1];
// 检查是否需要转向:下一步是否出界或已访问
if (nexti >= 0 && nexti < n && nextj >= 0 && nextj < m && !seen[nexti][nextj]) {
i = nexti;
j = nextj;
} else {
// 调整方向
dirIdx = (dirIdx + 1) % 4; // 四个方向循环
i += direction[dirIdx][0];
j += direction[dirIdx][1];
}
}
return res;
}
};
9.旋转图像
90度旋转
要求原地旋转,不另开数组
其实是个数学问题
先上下对称翻转,再主对角线翻转
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size() ;
//首先进行上下翻转
for( int i = 0 ; i < n/2 ; i ++ ){
swap(matrix[ i ], matrix[ n - i - 1]) ;
}
//然后进行对角线翻转
for( int i = 0 ; i < n ; i ++ ){
for( int j = i ; j < n ;j ++ ){
swap(matrix[i][j],matrix[j][i]) ;
}
}
}
};
10.删除数组中重复的元素II
有重复的全删掉,一个也不留
需要dummy node
初始化cur指向dummy node
每次循环看下一个节点和下下节点的值是不是一样的,如果是一样的,就嵌套一个while,不一样,cur就移动
这里嵌套while的方法无敌,仔细学
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* cur = dummy;
while(cur && cur->next && cur->next->next){
//先把这个值存下来
int num = cur->next->val;
if(cur->next->next->val == num){
//可能不止2个
while(cur->next && cur->next->val == num){
cur->next = cur->next->next;
}
}
else cur = cur->next;
}
return dummy->next;
}
};