LeetCode(12):longest-consecutive-sequence

题目描述

    Given an unsorted array of integers, find the length of the longest consecutive elements sequence.
    For example, Given [100, 4, 200, 1, 3, 2], The longest consecutive elements sequence is[1, 2, 3, 4]. Return its length: 4.
    Your algorithm should run in O(n) complexity.

思考

        理解本题其实并不难,无非就是要从一个没有排序的整数数组中确定最长的连续序列的长度。但是,难就难在对于时间复杂度的O(n)限定。如果本题不限制时间复杂度,我们肯定第一时间就是去把数组排序,然后遍历一下已经排序好的数组,找出最长的连续序列就行了,虽然我们也可以用一些线性时间复杂度的排序算法去实现,这里我们就不去探索了。
        我们先考虑下能不能用空间换时间,假如我们先new一个长度为maxN+1的内存p,然后遍历原数组,如果出现数a,则p[a] = p[a] + 1,最后对p进行遍历,找一下最长的连续不为0的段就行了。
        代码如下:

class Solution {
public:
    int longestConsecutive(vector<int> &num) {
        if(num.size() == 0){
            return 0;
        }
        if(num.size() == 1){
            return 1;
        }
        int maxN = num[0];
        int minN = num[0];
        for(int i = 1; i < num.size(); i++){
            if(num[i] > maxN){
                maxN = num[i];
            }
            if(num[i] < minN){
                minN = num[i];
            }
        }
        int len = maxN - minN + 1;
        int bias = -minN;
        int* p = new int[len]();
        for(int i = 0; i < num.size(); i++){
            p[num[i] + bias] += 1;
        }
        int longestN = 0;
        int temp = 0;
        for(int i = 0; i < len; i++){
            if(p[i] > 0){
                temp += 1;
            }
            else{
                if(temp > longestN){
                    longestN = temp;
                }
                temp = 0;
            }
        }
        delete[] p;
        return longestN;
    }
};

        其中值得注意的是对于负数的处理。很遗憾的是,这种方法并不能通过LeetCode,运行时将抛出一个std::bad_alloc的异常,以表明我们申请内存失败,原因是maxN可能会很大,超过了我们能够分配的内存大小,maxN最大可以是 (2311+1)32=236 ( 2 31 − 1 + 1 ) ∗ 32 = 2 36 个bits,也即 23GB 2 3 G B ,8个 G G ,LeetCode仅仅给我们提供了32768K,显然是不够的。
        我们应该怎么办?此时我们可以借助一些标准库中的常用数据结构,比如map,比如hash_map。
        下面我们先简要介绍C++标准库中的map。思考一个简单的例子:建立一个单词在文本中出现次数的列表。最明显的方式是我们维护一个列表,在看到每个单词之后将次数加起来。在这个过程中,我们不得不为读取的每个单词进行一次查找,对于普通的列表而言,这是缓慢的。而如果我们使用映射存储关键字的方式,则查找过程将会十分容易:

int main(){
    map<string, int>words;
    string s;
    while(cin>>s){
        ++words[s];
    }
    typedef map<string, int>::const_iterator Iter;
    for(Iter iter = words.begin(); iter != words.end(); ++iter){
        cout << iter->first << ":" << iter->second << endl;
    }
}

        上面这段代码便达到了我们的目标,其中++words[s]尤其精髓。
        好了,我们现在来用map把题目实现一下:

class Solution {
public:
    int longestConsecutive(vector<int> &num){
        map<int, int> table;
        typedef vector<int>::iterator Iter1;
        for(Iter1 iter = num.begin(); iter != num.end(); ++iter){
            ++table[*iter];
        }
        typedef map<int, int>::iterator Iter2;
        int maxLen = 0;
        for(Iter2 iter = table.begin(); iter != table.end(); ++iter){
            int temp = 1;
            int key = iter->first+1;
            while(table.find(key) != table.end()){
                temp++;
                key++;
            }
            if(temp > maxLen){
                maxLen = temp;
            }
        }
        return maxLen;
    }
};

        上面的代码可以在自己的IDE中跑,但是在LeetCode中,时间超出了限制,现在我们应该怎么办?我们可以思考一下上面的代码有什么可以优化的地方,给出一个序列[100, 4, 200, 1, 3, 2],我们将它转为map之后,它是有序的了,因为map使用红黑树实现的。这个时候我们的map是这样的[<1,1><2,1><3,1><4,1><100,1><200,1>],一开始我们的iter在Key=1的地方,然后找到maxLen=4,接着iter指向2,显然这时候我们又要计算一次1,2,3,4的这个序列,后面iter指向3、4又会算一遍,这并不是我们想要的。所以,我们可以修改如下:

class Solution {
public:
    int longestConsecutive(vector<int> &num){
        map<int, int> table;
        typedef vector<int>::iterator Iter1;
        for(Iter1 iter = num.begin(); iter != num.end(); ++iter){
            ++table[*iter];
        }
        typedef map<int, int>::iterator Iter2;
        int maxLen = 0;
        for(Iter2 iter = table.begin(); iter != table.end(); ++iter){
            int temp = 1;
            int key = iter->first+1;
            while(table.find(key) != table.end()){
                table.erase(table.find(key));
                temp++;
                key++;
            }
            if(temp > maxLen){
                maxLen = temp;
            }
        }
        return maxLen;
    }
};

        仔细观察上面的代码,可以发现,其中我们并没有用到map的键-值对的特性,所以我们下面改用set实现:

class Solution {
public:
    int longestConsecutive(vector<int> &num){
        set<int> table;
        typedef vector<int>::iterator Iter1;
        for(Iter1 iter = num.begin(); iter != num.end(); ++iter){
            table.insert(*iter);
        }
        typedef set<int>::iterator Iter2;
        int maxLen = 0;
        for(Iter2 iter = table.begin(); iter != table.end(); ++iter){
            int temp = 1;
            int key = *iter + 1;
            while(table.find(key) != table.end()){
                table.erase(table.find(key));
                temp++;
                key++;
            }
            if(temp > maxLen){
                maxLen = temp;
            }
        }
        return maxLen;
    }
};

        当然,我们也可以用unordered_set进行实现:

class Solution {
public:
    int longestConsecutive(vector<int> &num){
        unordered_set<int> table;
        typedef vector<int>::iterator Iter1;
        for(Iter1 iter = num.begin(); iter != num.end(); ++iter){
            table.insert(*iter);
        }
        typedef unordered_set<int>::iterator Iter2;
        int maxLen = 0;
        for(Iter2 iter = table.begin(); iter != table.end(); ++iter){
            int temp = 1;
            int key1 = *iter - 1;
            int key2 = *iter + 1;
            while(table.find(key1) != table.end()){
                table.erase(table.find(key1));
                temp++;
                key1--;
            }
            while(table.find(key2) != table.end()){
                table.erase(table.find(key2));
                temp++;
                key2++;
            }
            if(temp > maxLen){
                maxLen = temp;
            }
        }
        return maxLen;
    }
};

        这个时候,我们加入了一个往前搜索的操作,因为unordered_set是用哈希表实现的,所以是无序的,因此我们对于某个元素需要往前搜。正是由于unordered_set使用哈希表实现的,所以其时间复杂度为O(n),而使用map、set则是log(n),因而本题的最终解法是使用unordered_set。

        Ok,本题就解到这里。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值