算法闭关修炼百题计划(三)

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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值