Week8. 第141-150题

141. 环形链表

分析

链表找环
有环意味着从前往后遍历会遇到重复的点(重复是指地址重复)
最多只有1个环, 如果有很多环
图中一个蓝色点会有2个next, 分叉出去, 一个向下走, 一个向右走
在这里插入图片描述
其实每个点只有一个指针, 因此有环的话就只有1个环

开个hash表存下地址, 然后遍历的过程中判断有无出现重复的点, 如果有重复点, 说明有环
时间O(n), 空间O(n)

题目要求空间O(1)

快慢指针:
如果这样走

每次迭代 快指针与慢指针之间的距离k(图中绿色段)会-1, 所以迭代k次, 快指针会追上慢指针

笔记:(包含时间复杂度)
最坏快指针与慢指针距离n - x, 总的步数 3*(n - x ) + 2x + x = 3n, 还是O(n)
在这里插入图片描述

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if (!head || !head->next) return false;
        auto s = head, f = head->next;
        while (f){
            s = s->next, f = f->next; // 快慢同时走一步
            if (!f) return false; // 如果不能走, 直接返回
            f = f->next; // 可以的话, 继续走一步
            if (s == f) return true; // 如果相遇, 那就有环
        }
        return false;
    }
};

142. 环形链表 II

分析

返回环的入口

首先假设第一次相遇的时候慢指针走过的节点个数为i,设链表头部到环的起点的长度为m,环的长度为L,相遇的位置与起点与环的起点位置距离为k。
于是有:
i = m + a * L + k

其中a为慢指针走的圈数。
因为快指针的速度是慢指针的2倍,于是又可以得到另一个式子:
2 * i = m + b * L + k
其中b为快指针走的圈数。

两式相减得:
i = ( b - a ) * L
也就是说i是圈长的整数倍。

这时a节点放在s起点,b处于N相遇位置,然后a,b同时向前走m步时,此时从头部走的指针在m位置。
b而应该在距离起点i+m,i为圈长整数倍,则该指针也应该在距离起点为m的位置,即环的起点。
我们可以理解为b节点从起点开始走,走到环起点,然后绕环转了几圈。
————————————————
版权声明:本文为CSDN博主「清水雅然君」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/liuhao192/article/details/106356382

注意1

相遇点不一定在环的入口

相遇之后, 设入口b和相遇点c距离y, 考虑时间倒流, 直到把慢指针从c退到b, 那么快指针会回退到c’点

此时慢指针走了x步, 快指针2x到达c’

因此可以发现从b点走x步会走到c’(这x步可能转很多圈)
将起点b偏移到c(偏移y), 终点c’偏移到b(偏移y)
那么从c点开始走x步会到达b

然后让一个指针在起点, 一个指针在相遇点c一起走x步骤,
那么第一个指针到达的点就是环的入口
在这里插入图片描述
yxc: 我再讲一次
在这里插入图片描述

注意2

赋初值的时候, 快慢指针的位置在这里插入图片描述
其实他俩起点是在慢指针的前一个节点(虚拟点的位置)开始走的, 因此最后在第2次赋初值找入口的时候, 应该让点回到虚拟点的位置.

但代码中不用创建虚拟点, 只需要让快指针往后多走一步, 因为前面一个指针多走了一步, 快指针再走一步, 弥补一下

在这里插入图片描述

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if (!head || !head->next) return NULL;
        auto s = head, f = head->next;
        while (f){
            s = s->next, f = f->next;
            if (!f) return NULL;
            f = f->next;
            if (s == f){
                s = head, f = f->next; // 快指针错1位
                // while条件 s != f是精髓, 因为s重新赋值后, 在走x步之前, 不可能与f相遇, 当且仅当走了x步, 才相遇
                while (s != f) // 慢指针回到起始点开始走, 其实这里是起始点 + 1, 然后快指针已经+1, 弥补过了
                    s = s->next, f = f->next;
                return s;
            }
        }
        return NULL;
    }
};

143. 重排链表

分析

把1个链表, 交错相排, 交错的部分是倒着的后半部分

可以先将后半部分链表反向
然后分别从两个链表的头节点开始走, 边走边交错

考虑细节
有可能是奇数, 让中间点归到左边
所以找中点的时候, 应该是 ⌈ n 2 ⌉ = ⌊ n + 1 2 ⌋ \lceil \frac{n}{2} \rceil = \lfloor \frac{n + 1}{2}\rfloor 2n=2n+1
所以先找到图中两点, 再让相邻点指针翻转
在这里插入图片描述
yxc的备课代码更好

在这里插入图片描述

code

/**
 * 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:
    void reorderList(ListNode* head) {  
        if (!head) return ;
        int n = 0;
        for (auto p = head; p; p = p->next) n ++;
        auto mid = head;
        for (int i = 0; i < (n + 1) / 2 - 1; i ++ ) mid = mid->next; // 找中点

        auto tail = mid;
        for (auto p = mid, q = mid->next; q;){ 
            // 从中点, 中点后一个点开始翻转,
            // 从中点和后一个点开始翻转的原因是, 下一步循环结束条件, q!=mid, 往回找到mid截止
            auto next = q->next;
            q->next = p, p = q, q = next;
            tail = p;
        }

        for (auto p = head, q = tail; q != mid;){
            auto next = q->next;
            q->next = p->next;
            p->next = q;
            p = p->next->next, q = next;
        }
        if (n % 2) mid->next = NULL; 
        else mid->next->next = NULL; // 如果个数为偶数, 上面翻转完后的结果将会是mid->mid+1, 因此需要对mid+1->NULL
    }
};

144. 二叉树的前序遍历

分析

根左右
如果有当前点有右儿子的话, 那么将当前点的左链压入栈中
跟中序遍历很像

在遍历根节点的时候
中序遍历是先遍历完左子树后, 再遍历根节点
前序遍历是先遍根节点, 再遍历左子树

将当前节点压入栈中, 意味着我们要遍历左子树了, 在此之前,我们先遍历当前节点, 也就是自身

左儿子全部遍历完后, 根节点要去回溯的时候, 要回溯到它的后继节点
此时栈顶元素就是后继节点

迭代

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (root || stk.size()){
            while (root){ // 只要当前节点不空, 就将当前节点压入栈中
                // 压入栈中, 意味着要遍历左子树了, 在此之前, 先遍历当前点
                res.push_back(root->val); 
                stk.push(root); 
                root = root->left;
            }
            // 此时左子树遍历完了, 回溯到后继节点, 也就是上一个节点, stk.top(), 因为根节点遍历过了
            // 所以直接跳到right
            
            root = stk.top()->right; stk.pop();

        }
        return res;
    }
};

code(递归)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> ans;
    vector<int> preorderTraversal(TreeNode* root) {
        dfs(root);
        return ans;
    }

    void dfs(TreeNode* root){
        if (!root) return ;
        ans.push_back(root->val);
        dfs(root->left);
        dfs(root->right);
    }
};

145. 二叉树的后序遍历

分析

如果按照前序遍历和中序遍历的做法去做, 会发现比较麻烦
需要想办法记录额外信息

前序遍历在将当前点压入栈中的之前, 对应的是递归左子树, 对应到递归函数里, 相当于递归左子树
如果前序遍历的话, 在这个操作之前, 遍历根节点
如果中序遍历的话, 在栈里弹出这个点的时候, 遍历根节点, 就得到中序遍历

但是后序遍历不一样, 因为当前无法知道何时遍历完整棵子树了, 可以通过记录额外信息来获得遍历完子树的时间

但这题有一个简单的做法
一旦知道前序遍历的话

偷鸡法

如果我们按照前序遍历的方式 根 右 左的方式遍历, 得到的顺序是后序遍历左 右 根镜像的顺序

code

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (root || stk.size()){
            while (root){
                res.push_back(root->val);
                stk.push(root);
                root = root->right;
            }
            root = stk.top()->left; stk.pop();
        }

        reverse(res.begin(), res.end());
        return res;
    }
};

146. LRU 缓存机制

分析

可以把缓存看成hash表

对于put 和get 操作, 我们可以用hash表来维护缓存, 因为hash表增删改查都是O(1)的

要实现LRU机制, hash表中不仅要记录下来每个key对应的value是多少, 还需要记录下来哪个k的时间戳最小
要去修改每个位置的时间戳, 同时要快速找到时间戳最小的值 而且要O(1)时间

这里就要用到一个常用的数据结构—链表/或者可以理解为(队列)

比方说用了某个位置上的数的话, 就把这个位置上的数删掉,然后把它放到队头, 那么队尾元素一定是没有被用过的数
在这里插入图片描述

所以要支持两个操作, 快速的删掉一个位置上的数, 另外一个是将一个节点插入到最左侧

有一个数据结构叫做双链表

单链表不可以, 因为无法在O(1)时间删除, 因为我们删除的话, 需要知道它的上一个点, 对于单链表来说, 这个点是无法知道上一个点的, 所以不能O(1)的时间办到

所以这题我们用hash表维护key-value, 双链表维护时间戳, 相当于是用双链表做了一个队列, 每次用一个数, 就用这个节点上的数从双链表上删掉, 删完之后插到双链表的最左侧, 那这样的话, 双链表其实是一个排序的结果, 越往左越新, 越往右越旧, 每次如果爆掉缓存的话, 我们就把最后一个位置删掉

插入

在这里插入图片描述

在这里插入图片描述

删除

在这里插入图片描述
在这里插入图片描述

容量爆掉的话, 只会发生在put里的else分支

code

class LRUCache {
public:
    struct Node{
        int key, val;
        Node* left, *right;
        Node(int _key, int _val) : key(_key), val(_val), left(NULL), right(NULL) {}
    }*L, *R; // 注意这里的L是虚拟头节点, R是虚拟尾节点, 都是不放任何信息, L右侧才存Node, R左侧才存Node
    unordered_map<int, Node*> hash; // hash表维护节点Node信息
    int n;

    void remove(Node* p){
        p->right->left = p->left;
        p->left->right = p->right;
    }

    void insert(Node* p){
        p->right = L->right;
        p->left = L;
        L->right->left = p;
        L->right = p;
    }

    LRUCache(int capacity) {
        n = capacity;
        L = new Node(-1, -1), R = new Node(-1, -1);
        L->right = R, R->left = L;
    }
    
    int get(int key) {
        if (hash.count(key) == 0) return -1;
        auto p = hash[key];
        remove(p);
        insert(p);
        return p->val;
    }
    
    void put(int key, int value) {
        if (hash.count(key)){
            auto p = hash[key];
            p->val = value;
            remove(p);
            insert(p);
        }else {
            if (hash.size() == n){
                auto p = R->left;
                remove(p);
                hash.erase(p->key);
                delete p;
            }
            auto p = new Node(key, value);
            hash[key] = p;
            insert(p);
        }
    } 
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

147. 对链表进行插入排序

分析

类似打扑克,
6 5 3 1 8 7 2 4
第1个数6
6

第2个数5, 发现5应该放在6的前面

5 6

第3个数 3, 应该放在5的前面
3 5 6

第4个数1, 1的话应该放到3的前面
1 3 5 6

第5个8, 应该放到6的后面
1 3 5 6 8

第6个数7, 应该放到8的前面, 和6的后面
1 3 5 6 7 8

第7个数2, 应该放到1和3的中间
1 2 3 5 6 7 8

第8个数4, 应该放到3和5的中间
1 2 3 4 5 6 7 8

因此, 每次需要找到当前数应该插入的位置, 怎么找呢
也就是说找到第1个大于它的位置, 然后查到它的前面就可以了

因为前面维护的链表是有序的, 然后找到第一个比它大的节点, 停下来就可以在这里插入图片描述
当有有下一个点的时候, 并且下一个点的值比当前值小, 那么就可以一直往后走

总结:

code

记住: 要先开一个链表, 表示已经排好序了
然后遍历原链表,
再从维护好的链表中找到第1个大于当前值的位置, 在此之前(保存在当前点的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* insertionSortList(ListNode* head) {
        auto dummy = new ListNode(-1);
        for (auto p = head; p;){
            auto cur = dummy, next = p->next; // 已经排序好的链表dummy
            //  因为要将p插入到正确的位置, p->next会改变, 保存下, 遍历的时候需要找回
            while (cur->next && cur->next->val <= p->val) cur = cur->next; 
            // 这里结束循环, 表示cur->next比p大
            p->next = cur->next;
            cur->next = p;
            p = next;
        }
        return dummy->next;
    }
};

148. 排序链表

分析

快排 时间复杂度: 期望O(nlogn) 空间: 需要递归, 需要系统栈O(logn)
归并 时间复杂度: O(nlogn), 空间: 递归O(logn), 迭代的O(1)
堆排序 时间复杂度O(nlogn), 空间:O(1) (堆的存储方式) (链表不能用堆的存储方式(二叉树), 因此链表是不能存成堆的)

堆排序通过简单的交换就能把数据就地排成堆,不需要辅助空间。

所以这题只能用迭代的归并排序

从底下往上做, 就可以省掉递归栈的空间了O(1)
在这里插入图片描述
为了方便, 可以假设链表长度是2^n, 假如不是2^n(2的整数幂), 补充成2^n, 然后补充的部分就当不存在

在这里插入图片描述

code

/**
 * 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* sortList(ListNode* head) {
        int n = 0;
        for (auto p = head; p; p = p->next) n ++;
        // i = 1的时候表示 长度为1的区间合并 1 1合并成2, 那么当i = n的时候, 表示已经合并完成, 不需要合并成2n, 链表长度才n啊
        for (int i = 1; i < n; i *= 2 ){
            auto dummy = new ListNode(-1), cur = dummy; // 新建一个来存合并完的链表
            for (int j = 1; j <= n; j += i * 2){ // j <= n而不是j + i <= n, 是因为最后一段不足i的也要保存
                auto p = head, q = p; // 先找到成对的第2个长度为i的区间的头部
                for (int k = 0; k < i && q; k ++ ) q = q->next;
                auto o = q; // 保存 下一个长度为i的区间的头
                for (int k = 0; k < i && o; k ++ ) o = o->next;
                int l = 0, r = 0;
                while (l < i && r < i && p && q){
                    if (p->val <= q->val ) cur = cur->next = p, p = p->next, l ++;
                    else cur = cur->next = q, q = q->next, r ++ ;
                }
				// 因为会有长度不足2^n的区间(比如最后一段), 所以要加两个while
                while (l < i && p) cur = cur->next = p, p = p->next, l ++ ; 
                while (r < i && q) cur = cur->next = q, q = q->next, r ++ ;
                head = o;
            }
            cur->next = NULL; // 将合并完的区间段尾节点置空
            head = dummy->next; // 已经合并成2*i的区间, 让head指向合并完成的头部
        }
        return head;
    } 
};

149. 直线上最多的点数

分析

先想下暴力做法

  1. 先枚举直线 O(n^2): 枚举两个点对形成的直线
  2. 判断直线上有多少个点 O(n)

判断直线上有多少个点, 可以用直线方程, 叉积, 点积, 很多方式可以求
O(n^3)

可以优化

先枚举中心点O(n), 然后枚举下集合中其他点和该中心点的斜率O(n), 用hash表记录斜率为k的直线上的点的数量.
遍历完当前中心点后, 统计下最大值,
O(n^2)

code

vs : 垂直x轴的直线上点的数量
ss : 与中心点重合的点的数量

class Solution {
public:
    int maxPoints(vector<vector<int>>& points) {
        typedef long double LD;
        int res = 0;
        for (auto& p : points){
            unordered_map<LD, int> cnt;
            int ss = 0, vs = 0;
            for (auto& q : points){
                if (p == q) ss ++;
                else if (p[0] == q[0]) vs ++;
                else {
                    LD k = (LD) (q[1] - p[1]) / (LD)(q[0] - p[0]);
                    cnt[k] ++;
                }
            }
            int c = vs; // 垂直的也算斜率
            for (auto [k , t] : cnt) c = max(c, t); // 因此要拿垂直的直线与 其他斜率的直线比较点数
            res = max(res, c + ss);
        }
        return res;
    }
};

150. 逆波兰表达式求值

分析

逆波兰表达式定义就是不断将字符压入栈中, 遇到操作符就将栈顶2个计算下, 再压入栈中

从前往后遍历下

  1. 如果是操作符, 取出栈顶2个
  2. 否则, 即不是操作符 push(stoi(s))

stoi --> string to int

code

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> stk;
        for (auto& s : tokens){
            if (s == "+" || s == "-" || s == "*" || s == "/"){
                auto b = stk.top(); stk.pop(); // 注意 第1个top的是b
                auto a = stk.top(); stk.pop(); // 第2个才是a啊
                if (s == "+") a += b;
                else if (s == "-") a -= b;
                else if (s == "*") a *= b;
                else a /= b;
                stk.push(a);
            }else stk.push(stoi(s));
        }
        return stk.top();
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值