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

在这里插入图片描述

🏠关于专栏:专栏用于记录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;
    }
};

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值