LeetCode138. 复制带随机指针的链表/1893. 检查是否区域内所有整数都被覆盖/370. 区间加法(学习差分数组+前缀和、回顾树状数组、线段树)

138. 复制带随机指针的链表

2021.7.22 每日一题

题目描述

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。

示例 1:
在这里插入图片描述

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:

在这里插入图片描述

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:

在这里插入图片描述

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
示例 4:

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/copy-list-with-random-pointer
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

剑指offer的原题
这里的关键点是处理随机指针,我想到的思路是,将原节点和新节点对应,放在一个哈希表中,然后就能在一次遍历中快速的查到指向的节点。
在写的过程中,发现index指针是移动的,那么直接存index可以吗,有这个疑问。后面也想明白了,index就是一个原链表的节点,所以map存储的也是这个节点,而不是一个移动的指针,所以是可以的

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        //主要是随机指针怎么复制
        //这边用一个map,存储原节点对应的新节点
        Node dummy = new Node(-1);
        Node index = head;
        Node newid = dummy;
        Map<Node, Node> map = new HashMap<>();
        while(index != null){
            Node node = new Node(index.val);
            newid.next = node;
            newid = newid.next;
            //这样存行吗??????
            map.put(index, node);
            index = index.next;
        }

        index = head;
        newid = dummy.next;
        while(index != null){
            Node ran = index.random;
            Node newran = map.get(ran);
            newid.random = newran;
            newid = newid.next;
            index = index.next;
        }
        return dummy.next;

    }
}

另一个思路,将新建的节点放在原节点后面,这样做的好处是,可以很快找到对应的随机节点,也不用map的空间

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        //主要是随机指针怎么复制
        //这边用一个map,存储原节点对应的新节点
        if(head == null)
            return null;
        Node dummy = new Node(-1);
        Node index = head;

        
        //新节点接在原节点后面
        while(index != null){
            Node node = new Node(index.val);
            node.next = index.next;
            index.next = node;
            index = node.next;
        }

        //处理随机指针
        index = head;
        while(index != null){
            Node ran = index.random;
            if(ran != null)
                index.next.random = ran.next;
            index = index.next.next;
        }
        //分割两个链表
        index = head;
        dummy.next = head.next;
        Node id = dummy;
        while(index != null){
            id.next = index.next;
            id = id.next;
            index.next = id.next;
            index = index.next;
        }
        return dummy.next;

    }
}

1893. 检查是否区域内所有整数都被覆盖

题目描述

给你一个二维整数数组 ranges 和两个整数 left 和 right 。每个 ranges[i] = [starti, endi] 表示一个从 starti 到 endi 的 闭区间 。

如果闭区间 [left, right] 内每个整数都被 ranges 中 至少一个 区间覆盖,那么请你返回 true ,否则返回 false 。

已知区间 ranges[i] = [starti, endi] ,如果整数 x 满足 starti <= x <= endi ,那么我们称整数x 被覆盖了。

示例 1:

输入:ranges = [[1,2],[3,4],[5,6]], left = 2, right = 5
输出:true
解释:2 到 5 的每个整数都被覆盖了:

  • 2 被第一个区间覆盖。
  • 3 和 4 被第二个区间覆盖。
  • 5 被第三个区间覆盖。
    示例 2:

输入:ranges = [[1,10],[10,20]], left = 21, right = 21
输出:false
解释:21 没有被任何一个区间覆盖。

提示:

1 <= ranges.length <= 50
1 <= starti <= endi <= 50
1 <= left <= right <= 50

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/check-if-all-the-integers-in-a-range-are-covered
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

周赛做过的题,怎么最近老出周赛的题…
排序,遍历就行了

class Solution {
    public boolean isCovered(int[][] ranges, int left, int right) {
        Arrays.sort(ranges, (a, b) -> ((a[0] == b[0]) ? a[1] - b[1] : a[0] - b[0]));
        int index = left;
        int l = ranges.length;
        for(int[] range : ranges){
            int ll = range[0];
            int rr = range[1];
            if(index >= range[0] && index <= range[1]){
                index = range[1] + 1;    
                if(index > right){
                   return true;
                }
            }
        }
        return false;

    }
}
差分数组

学一下差分数组,没看懂官解是在说什么
差分数组,首先明确定义:差分数组 diff 维护相邻两个整数的被覆盖区间数量变化量
其中 diff[i] 对应覆盖整数 i 的区间数量相对于覆盖 i − 1 的区间数量变化量
啥意思呢,如果在区间开始位置 i ,那么diff[i]就加1,意思是就是说在这个位置相比于前面的位置,多了一个区间覆盖
如果在区间结束位置i,diff[i + 1]就减1,意思就是说i+1这个位置比i位置少一个区间覆盖

这样,对差分数组求前缀和,就可以得到每个位置被覆盖的次数

class Solution {
    public boolean isCovered(int[][] ranges, int left, int right) {
        //差分数组学习一下
        //首先明确定义:差分数组 diff 维护相邻两个整数的被覆盖区间数量变化量
        //其中 diff[i] 对应覆盖整数 i 的区间数量相对于覆盖 i − 1 的区间数量变化量
        //啥意思呢,如果在区间开始位置 i ,那么diff[i]就加1,意思是就是说在这个位置相比于前面的位置,多了一个区间覆盖
        //如果在区间结束位置i,diff[i + 1]就减1,意思就是说i+1这个位置比i位置少一个区间覆盖

        //这样,对差分数组求前缀和,就可以得到每个位置被覆盖的次数
        
        //总共0到50共51个数,因为可能i+1多出一个数,所以定义长度为52
        int[] diff = new int[52];
        for(int[] range : ranges){
            diff[range[0]]++;
            diff[range[1] + 1]--;
        }

        //计算前缀和
        int[] pre = new int[51];
        pre[0] = diff[0];
        for(int i = 1; i <= 50; i++){
            pre[i] = pre[i - 1] + diff[i];
        }

        for(int i = left; i <= right; i++){
            if(pre[i] == 0)
                return false;
        }
        return true;
    }
}
树状数组

看了三叶姐的题解,来补充一下树状数组的写法:
首先回顾树状数组的概念,一想到树状数组就会想到这张图:
在这里插入图片描述

在这个图中可以很清晰的看到树状数组的特征
树状数组必须要记住的一个操作就是lowbit = x&(-x),也就是取二进制表示中最低位的1
可以通过这个操作来更新数组中每个位置的数值
可以发现:(重点)
第一:设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素;
第二:二进制末尾0的个数,也表示该结点的层数,0个表示在最底层,1个表示在第一层,例如16,二进制末尾3个0,表示编号16的结点在第三层
第三:对于处于数组位置 i 的结点,其代表的信息区间为 [i - lowbit(i) + 1, i]
第四:设当前结点编号为x,从子节点到父节点,可以通过x + lowbit(x)计算得到父节点的编号,例如编号为9时,最大编号为16,计算得到的值为10,12,16;可以通过这个规律来更新结点的值
第五:通过树状数组来计算前缀和,可以通过计算与当前结点同层的,并比它小的所有节点(称为兄弟结点)的和得到;例如,计算下标(1,14)的和,那么即计算结点14,12,8的和
第六:如何通过当前结点的下标,计算兄弟结点的下标:x - lowbit(x)
第七:更新结点时,它的所有父节点要同时更新,且x是更新前后的差值

明确这些以后,再来想为什么能用树状数组做:因为区间是在更新的,然后可以用求两个前缀和之差的方式得到一个数是否出现过
可以根据自己的理解来写代码,这样印象深刻:
(这里在add方法中,写成i < n是因为取不到n)

class Solution {
    //首先给出树状数组三个方法
    int n = 52;
    int[] tree = new int[n];

    public int lowbit(int x){
        return x & (-x);
    }
    //添加的方法,在位置x添加u,它的父节点都要更新
    public void add(int x, int u){
        for(int i = x; i < n; i += lowbit(i)){
            tree[i] += u;
        }
    }
    //查询的方法,查询前缀和的方法
    public int query(int x){
        int res = 0;
        for(int i = x; i > 0; i -= lowbit(i))
            res += tree[i];
        return res;
    }

    public boolean isCovered(int[][] ranges, int left, int right) {
        int l = ranges.length;
        //创建树状数组
        for(int i = 0; i < l; i++){
            int ll = ranges[i][0];
            int rr = ranges[i][1];
            for(int k = ll; k <= rr; k++){
                add(k, 1);
            }
        }

        //查询每个数
        for(int i = left; i <= right; i++){
            if(query(i) - query(i - 1) == 0)
                return false;
        }
        return true;
    }
}

然后,因为树状数组这个方法中,如果区间重叠的话,每个数都会被加很多次。其实完全没有必要,因为我们要找的是这个数是否存在。
那么就可以把相同的数不要重复添加,从而得到一个只有1和0的树状数组,那么在查询的时候,就直接right 到left - 1范围查询就好了,如果每个数都存在,那么返回的就是r-l+1

class Solution {
    //首先给出树状数组三个方法
    int n = 52;
    int[] tree = new int[n];

    public int lowbit(int x){
        return x & (-x);
    }
    //添加的方法,在位置x添加u,它的父节点都要更新
    public void add(int x, int u){
        for(int i = x; i < n; i += lowbit(i)){
            tree[i] += u;
        }
    }
    //查询的方法,查询前缀和的方法
    public int query(int x){
        int res = 0;
        for(int i = x; i > 0; i -= lowbit(i))
            res += tree[i];
        return res;
    }

    public boolean isCovered(int[][] ranges, int left, int right) {
        int l = ranges.length;
        Set<Integer> set = new HashSet<>();
        //创建树状数组
        for(int i = 0; i < l; i++){
            int ll = ranges[i][0];
            int rr = ranges[i][1];
            for(int k = ll; k <= rr; k++){
                if(!set.contains(k)){
                    set.add(k);
                    add(k, 1);
                }
            }
        }

        //查询
        return right - left + 1 == query(right) - query(left - 1);
    }
}
线段树

然后来看线段树:
首先是一个二叉树,叶子结点是数组中每个元素的值,然后它们的父节点就是两个结点的值相加,得到一个区间的和。

线段树有三个关键点,第一是构建线段树,第二是修改元素的时候要更新线段树,第三是根据线段树进行区域和的检索

数据范围是n,那么数组长度是2n,数组后半部分存放的是数组中的每个数据。然后根据二叉树的特点,先更新tree[i+n],然后更新它的父节点tree[i] = tree[2n] + tree[2n + 1],一直往上更新到顶部
需要注意的是,因为下标的奇偶性不同,在更新和查询的时候需要注意奇偶性的问题
但是要明确的一点是,左子树肯定是偶数下标,右子树是奇数下标
例如只有5个数,那么第一个5,6,7,8,9就是后面的存放五个数的位置,那么6 7 组成一个树,8 9组成一个树,然后再与5结合;而不是56,78,9

具体实现看代码:

class Solution {
    //线段树
    int n = 52;
    int[] tree = new int[2 * n];

    
    //添加的方法,在位置x添加u,它的父节点都要更新
    public void add(int x, int u){
        int index = x + n;
        tree[index] += u;
        while(index > 0){
            //如果是偶数,那么是左子树
            if(index % 2 == 0){
                tree[index / 2] = tree[index] + tree[index + 1];
            //如果是奇数,那么是右子树
            }else{
                tree[index / 2] = tree[index - 1] + tree[index];
            }
            index /= 2;
        }
    }
    
    public int sumRange(int left, int right){
        int sum = 0;
        int ll = left + n;
        int rr = right + n;
        while(ll <= rr){
            //如果左边下标在右子树,那么单独加这个值
            if(ll % 2 == 1){
                sum += tree[ll];
                ll++;
            }
            //如果右边下标在左子树,那么单独加这个值
            if(rr % 2 == 0){
                sum += tree[rr];
                rr--;
            }
            ll /= 2;
            rr /= 2;
        }
        return sum;
    }
    public boolean isCovered(int[][] ranges, int left, int right) {
        int l = ranges.length;
        Set<Integer> set = new HashSet<>();
        //创建线段树
        for(int i = 0; i < l; i++){
            int ll = ranges[i][0];
            int rr = ranges[i][1];
            for(int k = ll; k <= rr; k++){
                if(!set.contains(k)){
                    add(k, 1);
                    set.add(k);
                }
            }
        }
        
        
        //查询
        return right - left + 1 == sumRange(left, right);
    }
}

看了一下三叶姐的线段树,发现不是这样写的,然后去学习了一下线段树,讲的很好:
https://www.bilibili.com/video/BV1cb411t7AM?from=search&seid=7095115582360789450
在这里插入图片描述

其实和上面的写法差不多,只不过是将这棵树变成了完全二叉树,我写的是迭代形式,而这里是递归(这个代码只是实现了一下线段树的三个方法,不适用于本题)

	//线段树
    int n = 52;
    int[] tree = new int[4 * n];
    int[] nums;         //给定的一个数组,根据这个数组构架线段树
    
    //建立树的方法,根节点下标为root == 1,l和r分别代表数据的范围
    public void build(int root, int l, int r){
        if(l == r){
            tree[root] = nums[l];
        }
        //递归左右子树
        int mid = l + r >> 1;
        //左子树根节点下标为root * 2
        int left_tree = root << 1;
        //右子树根节点下标为root * 2 + 1
        int right_tree = root << 1 | 1;
        build(left_tree, l, mid);
        build(right_tree, mid + 1, r);
        //更新当前节点的值
        tree[root] = tree[left_tree] + tree[right_tree];
    }
    
    //更新区间的值,下标为index,增量为inc
    public void update(int root, int l, int r, int index, int inc){
        //说明找到了这个点,更新
        if(l == r){
            nums[index] += inc;
            tree[root] += inc;
        }
        //找这个点在哪边,然后递归
        int mid = l + r >> 1;
        int left_tree = root << 1;
        int right_tree = root << 1 | 1;
        if(index >= l && index <= mid){
            update(left_tree, l, mid, index, inc);
        }else{
            update(right_tree, mid + 1, right, index, inc);
        }
        tree[root] = tree[left_tree] + tree[right_tree];
    }

    public int query(int root, int l, int r, int left, int right){
        //如果查询的区间不在当前范围内,直接返回0
        if(r < left || l > right)
            return 0;
        else if(l == r)
            return tree[root];
        else if(l >= left && r <= right)
            return tree[root];
        int mid = l + r >> 1;
        int left_tree = root << 1;
        int right_tree = root << 1 | 1;
        int sumleft = query(left_tree, l, mid, left, right);
        int sumright = query(right_tree, mid + 1, right, left, right);
        return sumleft + sumright;
    }

然后之前看线段树题解的时候,发现会有什么懒标记。
这个是针对区间更新的
那么这个懒标记是什么呢,就是当区间更新的时候,可能一个节点表示的区间都在要更新的范围内,那么就没有向下递归继续更新子节点,而是在这个节点打了个懒标记,表示这个节点下面的节点都需要更新,但是这个节点代表区间的子节点并没有直接更新。
而对于上面两种写法的线段树,第一种迭代,因为是从底向上修改的,设需要修改的范围是[left, right],所以先找到left和right对应的下标,然后向上,标记区间是否被修改过(修改了多少),查询的时候,根据标记来查询答案
而对于第二种递归的写法中,一般会有pushDown方法,来下推标记,使得叶子节点得到更新,这个以后再研究吧

370. 区间加法

题目描述

假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k​​​​​​​ 个更新的操作。

其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex … endIndex](包括 startIndex 和 endIndex)增加 inc。

请你返回 k 次操作后的数组。

示例:

输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]]
输出: [-2,0,3,5,3]
解释:

初始状态:
[0,0,0,0,0]

进行了操作 [1,3,2] 后的状态:
[0,2,2,2,0]

进行了操作 [2,4,3] 后的状态:
[0,2,5,5,3]

进行了操作 [0,2,-2] 后的状态:
[-2,0,3,5,3]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/range-addition
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

差分数组练习

class Solution {
    public int[] getModifiedArray(int length, int[][] updates) {
        //差分数组做一下
        //即对每个区间加上对应的inc
        int l = updates.length;
        int[] diff = new int[length + 1];
        for(int i = 0; i < l; i++){
            int left = updates[i][0];
            int right = updates[i][1];
            int inc = updates[i][2];
            diff[left] += inc;
            diff[right + 1] -= inc;
        }

        int[] res = new int[length];
        res[0] = diff[0];
        for(int i = 1; i < length; i++){
            res[i] = res[i - 1] + diff[i];
        }
        return res;
    }   
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 好的,我来用文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统,动态规划也广泛应用于各种优化算法,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列,第j个元素的值。 在LeetCode,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值