【Hot100算法刷题集】哈希-03-最长连续序列(含排序、哈希、并查集法未正确使用哈希表导致算法效率降低的分析)

news2024/11/23 23:48:45

在这里插入图片描述

🏠关于专栏:专栏用于记录LeetCode中Hot100专题的所有题目
🎯每日努力一点点,技术变化看得见

题目转载

题目描述

🔒link->题目跳转链接
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O ( n ) O(n) O(n) 的算法解决此问题。

题目示例

示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

题目提示

0 0 0 <= nums.length <= 1 0 5 10^5 105
− 1 0 9 -10^9 109 <= nums[i] <= 1 0 9 10^9 109

解题思路及代码

排序[1]

由示例2可知,连续数字序列中不能包含重复数字。这里先介绍排序方法,这个方法的时间复杂度不满足题意,但也能够解决当前问题。首先对nums数组进行排序,对排序后的nums数组进行遍历时,如果nums[currentIndex]==nums[currentIndex-1]表示出现了重复数字,则直接跳过,如果nums[currentIndex]==nums[currentIndex-1]+1表示前后两个元素可存在于同一连续序列中,如果nums[currentIndex]!=nums[currentIndex-1]+1表示前后两个元素不存在于同一连续序列中。下图是关于上述表述的解释示例:
在这里插入图片描述

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int maxLen = nums.size() > 0 ? 1 : 0;
        int len = 1;
        for(int i = 1; i < nums.size(); ++i)
        {
            if(nums[i - 1] + 1 == nums[i]) ++len;
            else if(nums[i - 1] == nums[i]) continue;
            else len = 1;
            maxLen = max(maxLen, len);
        }
        return maxLen;
    }
};

哈希表[2]

上面的算法时间效率是 O ( N l o g N ) O(NlogN) O(NlogN),有没有时间效率更高的算法呢?我们可以将所有元素先保存到哈希表中,检测 n u m − 1 num-1 num1是否存在于哈希表中,若不在于哈希表中,则表明num为连续序列的起始元素;此时从num开始依次寻找num+1、num+2、…是否存在于哈希表中,即可得到以num为起始元素的连续序列长度。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> se;
        for(auto num : nums) se.insert(num);
        int maxLen = 0;
        for(auto num : se)
        {
            if(!se.contains(num - 1))
            {
                int len = 1;
                int i = 1;
                while(se.contains(num + i))
                {
                    ++len;
                    ++i;
                }
                maxLen = max(maxLen, len);
            }
        }
        return maxLen;
    }
};

上面算法的时间复杂度为 O ( N ) O(N) O(N),但如果我们在第二次循环时不是对unordered_set进行遍历,而是对nums进行遍历的话,算法的效率就会有所下降,因为unordered_set已经对元素进行了去重操作,而原数组nums中存在重复元素。

下面再看一个同样使用哈希表,但解法更为精妙的方式。为了更好解释这个方法,下面通过给出一个示例和图示来协助解释。

整体逻辑如下,创建一个unordered_map对象实例m作为哈希表,键域(key)存储nums中的元素,值域(value)存储一个数字,该数字用于协助更新连续序列的长度。当要将一个nums中的元素num插入哈希表时,先获取num-1和num+1对应的值域(value),这两个值表示num-1当前所在的最长连续序列的长度,num+1当前所在的最长连续序列的长度,将m[num-1]记为left,m[num+1]记为right,通过计算left+right+1得到当前元素num所在最长连续序列的长度;同时更新位于最左和最右的元素,使其值域(value)为当前的最长序列长度。每次执行上述操作,就使用max函数记录当前最长连续序列的长度,即可得到最终结果。

上述的逻辑本质就是,获取我当前元素的num-1和num+1所在的序列长度,通过这两个长度加上我当前元素的长度1,即可得到当前元素所在序列的最大长度。更新m[num-left]和m[num+right]为当前序列最大长度的原因是,num-left到num+right这个区间的元素不会被重复加入到哈希表,但num-left-1和num+right+1在后续执行逻辑中,可能会被加入到哈希表。为了让num-left-1获取它的相近右元素所在序列的长度,让num+right+1获取它的相近左元素所在序列的长度,故需要更新m[num-left]和m[num+right]为当前序列最大长度。
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_map<int, int> m;
        int maxLen = 0;
        for(auto num : nums)
        {
            if(m.find(num) != m.end()) continue;
            int left = m.find(num - 1) == m.end() ? 0 : m[num - 1];
            int right = m.find(num + 1) == m.end() ? 0 : m[num + 1];
            int len = left + right + 1;
            m[num] = len;
            m[num - left] = m[num + right] = len;
            maxLen = max(maxLen, len);
        }
        return maxLen;
    }
};

哈希表结合并查集[3]

下面介绍第三种方法——并查集。💡:如果对这个数据结构不是特别熟悉,可以看一下如下小tips,如果已经掌握该数据结构世界跳转到【🚀】图标所在的位置。

并查集可以理解成借助数组下标和数组所存储的值所构建的树形结构,且并查集表示的是多颗树所构成的森林。对于非根结点,它的数组元素只保存的是其父节点的下标;对于根节点,保存的是一个特定的值。举个例子,下图上方有两颗树,可以将它们两通过一个数组结构进行存储;由于数组下标大于等于0,根节点的父节点使用不存在的下标进行表示,这里使用-1表示,则下标为1和2的位置存储value为-1;其他节点则存储当前节点的父亲节点的下标,例如:7的父亲节点是4,则下标为7的位置存储的是4、0的父亲节点是2,则下标为0的位置存储value为2。
在这里插入图片描述
如果存储的值并不连续,例如存储数据的二叉树中序遍历为[1,500,10000],那么要开辟10000的数组显然十分浪费空间。则我们可以将上述的数组结构替换为vector<pair<int, int>>结构,其中pair<int, int>的first存储节点的父亲节点下标,second存储节点所存储的值。除此之外,我们还可以使用unordered_map的kv哈希表结构进行存储,key存储节点的父亲节点的key值,value存储当前节点的数值。

💡attention:下方介绍并查集的相关操作,使用的是unordered_map<int, int>哈希表结构ufs进行存储。

下面介绍三个并查集的操作,首先是:查找根节点。若已知一个节点的值,则可以借助该值,找到它的父亲节点;设节点的值为value,则它的父亲节点为ufs[value]。若要继续找父亲节点的父亲节点,则可执行value = ufs[value]; value = ufs[value];,第一次执行找到的是当前节点的父亲节点,第二次执行找到的是父亲节点的父亲节点,以此类推即可找到根节点。

unordered_map<int, int> ufs;

int findRoot(int value)
{
	while(ufs[value] != -1)
	{
		value = ufs[value];
	}
	return value;
}

第二个操作是合并两颗树,由于并查集中并不要求所存储的树是二叉树(即可以存储三叉、四叉等多叉树),我们可以先找到两颗树的根节点,将一棵树的根节点的父亲节点改为另一棵树的根节点。举个栗子🙋‍:将上述两颗树合并为一棵树。则需要先使用上面的找到根节点的逻辑,找到左树的根节点为1,右树的根节点为2。若需要将右树合并到左树,则修改下标为2所存储的值,即修改为1。相反的,如果需要将左侧的树合并到右侧的树,则需要将下标为1所存储的值修改为2。
在这里插入图片描述

unordered_map<int, int> ufs;

void mergeTree(int x, int y)
{
	x = findRoot(x);
	y = findRoot(y);
	ufs[y] = x;
}

第三个操作是确定两个节点是否位于同一颗树中。若位于同一颗树中,则它们两个的根节点的key值或数组下标将是相等的,相反是不相等的。

bool isInSameTree(int x, int y)
{
	x = findRoot(x);
	y = findRoot(y);
	return x == y;
}

🚀并查集解题逻辑:使用两个unordered_map容器实例化的哈希表ufs和cnt,其中ufs保存num元素之间的树形结构关系,cnt保存当前节点的子节点数量(这个数值等于该节点所在最长序列的长度)。这里由于节点数值范围为 − 1 0 9 -10^9 109 1 0 9 10^9 109,则使用INT_MIN(-2147483648)作为根节点的value域数值。首先将所有nums中的数值存储到ufs中,每个num都是一颗独立的树;再对nums进行遍历,遍历过程中,如果num+1存在,则将num+1合并到num所在的树中,找到num+1和num的根节点root(num+1)、root(num),将cnt[root(num)] += cnt[root(num + 1)]。

这里可能还是不好理顺,再使用简单话语对上述逻辑进行描述:这里使用unordered_map结构进行存储,若存在重复元素,将会在插入时自动去重;ufs将本应该位于同一连续序列的元素,串入一颗树中;使用cnt计算元素所在的连续序列的长度;如果遍历到num时,num-1存在,则表示两者为连续相邻元素,修改ufs,将两者所在的树合并,同时修改根节点cnt的数值。在修改cnt时,修改的都是小的数据所在的树的根节点的cnt,即小数据所在连续序列的最小元素,由它保存着当前连续序列的长度。

🙏:这里的解释方式若有不理解的地方,欢迎评论或私信讨论。若有更佳的解释方法,也欢迎一起讨论。

class Solution {
public:
    unordered_map<int, int> ufs, cnt;

    int find(int num)
    {
        while(ufs[num] != INT_MIN)
        {
            num = ufs[num];
        }
        return num;
    }
    int merge(int x, int y)
    {
        x = find(x); y = find(y);
        if(x == y) return cnt[x];
        ufs[y] = x;
        cnt[x] += cnt[y];
        return cnt[x];
    }
    int longestConsecutive(vector<int>& nums) {
        if(nums.size() == 0) return 0;
        for(int num : nums) ufs[num] = INT_MIN, cnt[num] = 1;
        int maxLen = 1;
        for(int num : nums)
        {
            if(ufs.count(num + 1)) maxLen = max(maxLen, merge(num, num+1));
            cout << maxLen << endl;
        }
        return maxLen;
    }
};

刷题使我快乐😭
文章如有错误,请私信或在下方留言😀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2120962.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记录一下linux安装nginx,也是很简单了啦

1、下载nginx 官网下载nginx&#xff1a;http://nginx.org/&#xff0c;这里很简单&#xff0c;下载自己想要的版本就行&#xff0c;这里不罗嗦 1、进入home目录&#xff0c;建一个文件夹nginx rootroot ~]# cd /home rootroot home]# mkdir nginx rootroot home]# cd /nginx2…

什么是智慧箱变动环辅控系统?箱式变电站动环监控@卓振思众

智慧箱变动环辅控系统是一种智能化的辅助控制系统&#xff0c;主要用于对箱变&#xff08;箱式变电站&#xff09;等设备的运行环境进行监测和控制。以下是【卓振思众】智慧箱变动环辅控系统的主要功能特点&#xff1a; 一、环境监测方面温度监测&#xff1a;实时监测箱变内的温…

C#编写上位机通过OPC DA读取西门子PLC数据

Sync_RW 引用&#xff1a;Quick.OpcComRcw 文档中原程序会报错&#xff1a; 原因&#xff1a;需要在安装有Simatic NET V14的电脑上运行这个程序。 需要注释掉下面程序&#xff0c;否则读取时会无故障提示退出。 //finally里的程序要注释掉&#xff0c;否则一点击read按钮&a…

招商银行信用卡中心编程练习题题解(全)

第一天 递归:LeetCode21.合并两个有序链表 /*** 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…

掌握生成树 (STP、RSTP、MSTP) 最详细配置,轻松优化网络

目录 一. 实验内容 STP配置实验 RSTP配置实验 MSTP配置实验 二. 1 ) STP配置实验 实验拓扑 ​编辑 实验配置 实验结果 2 ) RSTP配置实验 实验拓扑 实验配置 实验结果 3 ) MSTP配置实验 实验拓扑 实验配置 ​编辑 实验结果 三 实验总结 一. 实验内容 1) …

齐齐哈尔医院系统等保测评,安全防护全面升级

随着数字化时代的到来&#xff0c;医疗系统的信息化建设日益重要。然而&#xff0c;随之而来的数据安全问题也成为了医疗机构必须面对的重大挑战。为了保障患者信息的安全以及整个医疗系统的稳定运行&#xff0c;齐齐哈尔市某医院近期进行了等保&#xff08;等级保护&#xff0…

vue-router 之如何在模版(template)中获取路由配置信息?

vue-router 之如何在模版&#xff08;template&#xff09;中获取路由配置信息&#xff1f; 获取当前路由信息 在vue3 中&#xff0c;route通常使用useRoute()钩子获取的&#xff0c;**代表当前激活的路由信息。**它包含了与当前路由相关的数据&#xff0c;比如路径、参数、查…

小米对讲机2S申请业余无线电台执照

首先&#xff0c;小米对讲机2S是可以用来申请业余无线电台执照得&#xff0c;对讲机、电台在工信部能查到核准代码即可。 工业和信息化部政务服务平台 在小米对讲机2S底部有核准代码 2022FP10742&#xff0c;搜索后即可看到详细信息。 有一点矛盾的是&#xff0c;在这里查询到…

ML 系列:机器学习和深度学习的深层次总结(02)线性回归

ML 系列&#xff1a; — 简单线性回归 线性回归、损失函数、假设函数 图 1.线性回归 文章目录 一、说明二、线性回归2.1 简单线性回归2.2 回归中的损失函数 三、线性回归中的评估标准四、线性回归模型训练五、可视化5.1 假设函数5.2 计算训练模型的损失&#xff1a;5.3 绘制 H…

关于武汉芯景科技有限公司的IIC电平转换芯片XJ9517开发指南(兼容PCF9517)

一、芯片引脚介绍 1.芯片引脚 2.引脚描述 二、系统结构图 三、功能描述 1.电平转换 2.芯片使能/失能 EN 引脚为高电平有效&#xff0c;内部上拉至 VCC&#xff08;B&#xff09;&#xff0c;允许用户选择中继器何时有效。这可用于在上电时隔离行为不良的从机&#xff0c;直到…

饭店起名|饭店怎么起名有创意

给饭店起名是一个非常重要的任务&#xff0c;对于饭店的经营和吸引力都有着重要的影响。一个好的饭店名字能够吸引顾客、传达出饭店的风格和特色。在起名之前&#xff0c;我们需要先考虑一些因素&#xff0c;例如饭店的定位、菜品特色、目标顾客、所在地区等。下面是一些建议可…

20章 线性表、栈、队列和优先队列

1.编写一个程序&#xff0c;从文本文件读取单词&#xff0c;并按字母的升序显示所有的单词(可以重复)。单词必须以字母开头。文本文件作为命令行参数传递。 import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays;pu…

python-数字反转

题目描述 给定一个整数 N&#xff0c;请将该数各个位上数字反转得到一个新数。新数也应满足整数的常见形式&#xff0c;即除非给定的原数为零&#xff0c;否则反转后得到的新数的最高位数字不应为零&#xff08;参见样例 2&#xff09;。 输入格式 一个整数 N。 输出格式 …

数据结构基础讲解(七)——数组和广义表专项练习

本文数据结构讲解参考书目&#xff1a; 通过网盘分享的文件&#xff1a;数据结构 C语言版.pdf 链接: https://pan.baidu.com/s/159y_QTbXqpMhNCNP_Fls9g?pwdze8e 提取码: ze8e 数据结构基础讲解&#xff08;六&#xff09;——串的专项练习-CSDN博客 个人主页&#xff1a;樱娆…

猫头虎分析:iPhone 16 系列哪款更适合你?买 iPhone 16 选哪款好?

猫头虎分析&#xff1a;iPhone 16 系列哪款更适合你&#xff1f;买 iPhone 16 选哪款好&#xff1f; 大家好&#xff0c;我是猫头虎。每年苹果发布新机型时&#xff0c;总能引发广泛的讨论和期待。今年也不例外&#xff0c;苹果发布了备受瞩目的 iPhone 16 系列&#xff0c;无…

uniapp媒体

uni.previewImage实现图片放大预览 // 图片预览函数function onPreview(index) {// 收集所有图片的urlvar urls pets.value.data.map(item > item.url)// 预览图片uni.previewImage({current: index, // 当前预览的图片索引urls: urls // 所有图片的url数组})}

COCOS:(飞机大战02)简单的帧动画制作

飞机大战知识点总结 先准备2张图片 选中飞机Body&#xff0c;01&#xff1a;添加动画组件&#xff0c;02&#xff1a;新建动画剪辑资源&#xff0c;保存到动画目录。 在动画编辑器中先增加属性&#xff0c;点加号&#xff0c;选择cc.Sprite,选择spriteFrame把飞机图片hero0 拖…

项目售后服务方案(Word原件2024)

售后服务体系 售后服务流程 售后服务承诺 售后服务计划 技术支持响应承诺 售后服务响应时间 1.2 项目培训方案 项目培训体系 项目培训管理 培训目的与措施 项目培训安排 培训告知下达 培训人员贯彻 培训签到表 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审…

聚观早报 | 拼多多“百亿减免”新政策;石头洗地机A30系列发布

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 9月10日消息 拼多多“百亿减免”新政策 石头洗地机A30系列发布 iQOO 13将采用京东方定制屏幕 iPhone 16 Pro或支…

Lombok失效:报错 找不到符号 Springboot项目

错误原因&#xff0c;Springboot项目为Lombok提供了版本管理的支持&#xff0c;所以引入Lombok依赖的时候&#xff0c;无需手动指定版本&#xff0c;手动指定了可能会导致依赖冲突。 去掉手动指定的版本&#xff0c;问题解决