程序员进阶之算法练习(三十五)LeetCode专场

前言

LeetCode上的题目是大公司面试常见的算法题,今天的目标是拿下5道算法题:
题目1是基于链表的大数加法,既考察基本数据结构的了解,又考察在处理加法过程中的边界处理;
题目2是求数组出现频率前k大的数字,考察思维能力,代码很短;
题目3是给出从两个数组中选择数字,组成一个最大的数字,考察的是贪心的思想;
前三个都偏向于考察想法,实现的代码都比较简单;
题目4、5是数据结构实现题,也是大部分人比较头疼的题目,因为需要较多的数据结构和STL实现,并且还有时间和空间的限制。

正文

1、Add Two Numbers II

题目链接
题目大意

给俩个链表,节点由0~9的数字组成,分别表示两个数字;
求出两个数字的和,以链表的形式返回。

例如
Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)

7243 + 564 =7807

Output: 7 -> 8 -> 0 -> 7

题目解析:
题目的意思很明显,就是把两个数字加起来,需要考虑进位的情况。
因为是单向的链表,遍历后很难回溯,所以先把数字存到vec中。
并且为了处理方便,vec的最低位存在vec的起始部分。
于是从0开始遍历两个vec即可,注意考虑最后进位的情况。

复杂度解析:
时间复杂度是O(N)
空间复杂度是O(N)


struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *ret = NULL;
        vector<int> vec1, vec2;
        sum(l1, vec1);
        sum(l2, vec2);
        int n = vec1.size(), m = vec2.size(), flag = 0;
        for (int i = 0; i < n || i < m; ++i) {
            int x = 0, y = 0;
            if (i < n) {
                x = vec1[i];
            }
            if (i < m) {
                y = vec2[i];
            }
            int s = x + y + flag;
            if (s > 9) {
                s -= 10;
                flag = 1;
            }
            else {
                flag = 0;
            }
            ListNode *tmp = new ListNode(s);
            tmp->next = ret;
            ret = tmp;
        }
        if (flag) {
            ListNode *tmp = new ListNode(1);
            tmp->next = ret;
            ret = tmp;
        }
        return ret;
    }
    
    void sum(ListNode* list, vector<int> &vec) {
        if (list->next) {
            sum(list->next, vec);
        }
        vec.push_back(list->val);
    }
};

2.Top K Frequent Elements

题目链接
题目大意

给出一个数组和一个数字k,返回按数字出现频率的前k个的数字;
1 <= k <= n, n是数组大小;

 example,
 Given [1,1,1,2,2,3] and k = 2, return [1,2].

题目解析:

题目分为两个步骤:
1、统计每个数字的出现次数;
2、从中选择k个次数最多的数字;

一个简单的做法:
用哈希表统计每个数字的出现次数;
把每个数字的出现次数和数字组成一个pair,放入优先队列;

这样从优先队列中取出k个即可。

复杂度解析:
时间复杂度是O(NlogN),主要在最后的优先队列。

其他解法:
有一个O(NlogK)的优化;
首先把队列变成最小有限队列,
每次pair放入优先对时,如果当前的size大于k,那么弹出top;
这样每次的操作从O(logN)变成O(logK)。


class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> numsHash;
        for (int i = 0; i < nums.size(); ++i) {
            ++numsHash[nums[i]];
        }
        priority_queue<pair<int, int>> q;
        for (int i = 0; i < nums.size(); ++i) {
            if(numsHash[nums[i]]) {
                q.push(make_pair(numsHash[nums[i]], nums[i]));
                numsHash[nums[i]] = 0;
            }
        }
        vector<int> ret;
        for (int i = 0; i < k; ++i) {
            ret.push_back(q.top().second);
            q.pop();
        }
        return ret;
    }
}leetcode;

3、create-maximum-number

题目链接
题目大意
给出两个数组,数组只包括0~9十个数字,长度分别为n、m;
从两个数组中选出k个数,组成一个长度为k的数字,要求:
1、从数组n、m选择出来的数字相对位置不变;
2、最后的数字最大;
输出最后的数字。

 Example 1:
 nums1 = [3, 4, 6, 5]
 nums2 = [9, 1, 2, 5, 8, 3]
 k = 5
 return [9, 8, 6, 5, 3]
 
 Example 2:
 nums1 = [6, 7]
 nums2 = [6, 0, 4]
 k = 5
 return [6, 7, 6, 0, 4]

题目解析:

要求最后数字最大,那么尽可能把数字大的排在前面;
在都合法的前提下,99* 肯定比 98*要大;
那么可以按照这样的贪心策略:
先枚举t,t表示从数组nums1中选出t个数字,那么数组nums2中应该选出k-t个数字;
两个数组的所有数字组成最大的数字,因为两个数组间的数字是可以任意顺序,那么只需每次选择较大的放在前面即可。

问题简化成,O(N)每次从数组中选出t个最大的数字;
这个可以用贪心解决:
假设数组当前枚举到第i个,且nums[i]=x;
从左到右遍历已经选择的数,当遇到一个数字t,t<x时,判断插入x后,后续是否存在合法解;如果存在则替换,否则直到最后,插入尾部;


class Solution {
public:
    vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
        int n = (int)nums1.size(), m = (int)nums2.size();
        vector<int> ret(k, 0);
        for (int i = max(0, k - m); i <= k && i <= n; ++i) {
            vector<int> tmp1 = maxArray(nums1, i);
            vector<int> tmp2 = maxArray(nums2, k - i);
            vector<int> tmp = merge(tmp1, tmp2, k);
            if (greater(tmp, 0, ret, 0)) {
                ret = tmp;
            }
        }
        return ret;
    }
    
    vector<int> maxArray(vector<int> &nums, int k) {
        int n = (int)nums.size();
        vector<int> ret(k, 0);
        for (int i = 0, j = 0; i < n; ++i) {
            while (n - i + j > k && j > 0 && ret[j - 1] < nums[i]) {
                --j;
            }
            if (j < k) {
                ret[j++] = nums[i];
            }
        }
        return ret;
    }
    
    vector<int> merge(vector<int>& nums1, vector<int>& nums2, int k) {
        vector<int> ret(k, 0);
        for (int i = 0, j = 0, r = 0; r < k; ++r) {
            ret[r] = greater(nums1, i, nums2, j) ? nums1[i++] : nums2[j++];
        }
        return ret;
    }
    
    bool greater(vector<int> &nums1, int i, vector<int> &nums2, int j) {
        while (i < nums1.size() && j < nums2.size() && nums1[i] == nums2[j]) {
            ++i;
            ++j;
        }
        return j == nums2.size() || (i < nums1.size() && nums1[i] > nums2[j]);
    }
};

4、 Insert Delete GetRandom O(1) - Duplicates allowed

题目链接
题目大意
实现一个数据结构,包括以下三个方法:
1、insert(val): 插入一个数字;
2、remove(val): 移除一个数字;
3、getRandom: O(1)随机返回一个数字;


 Example
 插入数字1;
 collection.insert(1);
 插入数字1:
 collection.insert(1);
 插入数字2
 collection.insert(2);
 随机返回数字,要求 2/3可能返回1, 1/3可能返回2;
 collection.getRandom();

题目解析:

插入和移除数字不麻烦,考虑如何在O(1)时间返回一个数字。
容易知道,放在数组里面可以,然后随机返回一个位置可以实现。
增加可以在数组最末端增加;
删除数组中间某个数字时,可以把最末端的数字放到删除的位置上;

现在的问题是,如何快速找到数组中该删除的某个位置;
考虑用hash来实现。
数组就是vector<pair<int, int> >; first存val,second存出现次数;
再用一个哈希map,unordered_map<int, vector<int>> 里面存对应数字出现的位置;


class RandomizedCollection {
public:
    /** Initialize your data structure here. */
    RandomizedCollection() {
        
    }
    
    /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */
    bool insert(int val) {
        bool ret = hashMap.find(val) == hashMap.end();
        hashMap[val].push_back(randVec.size());
        randVec.push_back(make_pair(val, hashMap[val].size() - 1));
        return ret;
    }
    
    /** Removes a value from the collection. Returns true if the collection contained the specified element. */
    bool remove(int val) {
        bool ret = hashMap.find(val) != hashMap.end();
        if (ret) {
            auto last = randVec.back();
            hashMap[last.first][last.second] = hashMap[val].back();
            randVec[hashMap[val].back()] = last;
            hashMap[val].pop_back();
            if (hashMap[val].empty()) {
                hashMap.erase(val);
            }
            randVec.pop_back();
        }
        return ret;
    }
    
    /** Get a random element from the collection. */
    int getRandom() {
        return randVec[rand() % randVec.size()].first;
    }
    
private:
    unordered_map<int, vector<int>> hashMap;
    vector<pair<int, int>> randVec;
}leetcode;

5、 All O`one Data Structure

题目链接
题目大意

实现一个数据结构,要求:
1、Inc(Key) - Inserts a new key with value 1. Or increments an existing key by 1. Key is guaranteed to be a non-empty string.
2、Dec(Key) - If Key's value is 1, remove it from the data structure. Otherwise decrements an existing key by 1. If the key does not exist, this function does nothing. Key is guaranteed to be a non-empty string.
3、GetMaxKey() - Returns one of the keys with maximal value. If no element exists, return an empty string "".
4、GetMinKey() - Returns one of the keys with minimal value. If no element exists, return an empty string "".

要求所有的数据结构的时间复杂度是O(1);

题目解析:

在不考虑复杂度的前提下,朴素做法是遍历,O(N);
简单的优化,用map来维护优先队列,操作1、2先获取key值,更新完重新插入;操作3、4直接拿队列top;每个操作的复杂度是O(LogN);

题目要求是O(1),那么必然不能使用树类型的结构,应该利用题目特性,配合hash以及贪心来实现。

假设有一个key-hash表,来存key的对应值。
操作1、先看keyHash里面是否有key,有则+1,无则插入;
操作2、先看keyHash里面是否有key,有则-1,无则Nothing;

为了维护最值,引入链表list,里面所有的元素是从小到大;每个元素是一个桶,桶里放着值相同的key;
操作3、直接获取list头元素的值;
操作4、直接获取list尾元素的值;

同时,操作1、2在操作的过程中,需要把当前key值从list对应的桶里移除,放到上一个或者下一个桶里,或者丢弃。
为了实现O(1)获取key所在位置,可以用iter-hash来维护key所对应元素的迭代器。


struct Bucket {
    int value;
    unordered_set<string> keys;
};

class AllOne {
public:
    list<Bucket> buckets;
    unordered_map<string, list<Bucket>::iterator> bucketOfKey;
    /** Initialize your data structure here. */
    AllOne() {
        
    }
    /** Inserts a new key <Key> with value 1. Or increments an existing key by 1. */
    void inc(string key) {
        if (bucketOfKey.find(key) == bucketOfKey.end()) {
            bucketOfKey[key] = buckets.insert(buckets.begin(), {0, {key}});
        }
        auto next = bucketOfKey[key], bucket = next++;
        if (next == buckets.end() || next->value > bucket->value + 1) {
            next = buckets.insert(next, {bucket->value+1, {}});
        }
        next->keys.insert(key);
        bucketOfKey[key] = next;
        
        bucket->keys.erase(key);
        if (bucket->keys.empty()) {
            buckets.erase(bucket);
        }
    }
    
    
    /** Decrements an existing key by 1. If Key's value is 1, remove it from the data structure. */
    void dec(string key) {
        if (bucketOfKey.find(key) == bucketOfKey.end()) {
            return ;
        }
        auto pre = bucketOfKey[key], bucket = pre;
        if (pre != buckets.begin()) {
            --pre;
        }
        
        bucketOfKey.erase(key);
        if (bucket->value > 1) {
            if (bucket == buckets.begin() || pre->value < bucket->value - 1) {
                pre = buckets.insert(bucket, {bucket->value - 1, {}});
            }
            pre->keys.insert(key);
            bucketOfKey[key] = pre;
        }
        
        bucket->keys.erase(key);
        if (bucket->keys.empty()) {
            buckets.erase(bucket);
        }
    }
    
    /** Returns one of the keys with maximal value. */
    string getMaxKey() {
        return buckets.empty() ? "" : *(buckets.rbegin()->keys.begin());
    }
    
    /** Returns one of the keys with Minimal value. */
    string getMinKey() {
        return buckets.empty() ? "" : *(buckets.begin()->keys.begin());
    }
}leetcode;

总结

算法重在勤思多练,也基本都是面试前才会花很多时间去练习。
最近在忙新项目,积累了很多新的感触,但是还没时间去整理出来,只能先更新算法练习。
每天中午饭后一道medium,能坚持一年也会有上百道题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 前言 BAT常见的算法面试题解析:程序员算法基础——动态规划程序员算法基础——贪心算法工作闲暇也会有在线分享,算法...
    落影loyinglin阅读 1,053评论 0 7
  • 高先生虽然穿衣打扮比较直男 但声音还是蛮好听的 不然也不会被从小夸到大 还被选进了辩论队,打了说话最多的辩位 所以...
    某高阅读 220评论 2 1
  • 1. Rest 1.1 序列化json自动命名: json中如果有下划线命名的key,例如 user_name...
    Juude阅读 417评论 1 0
  • 描述 人民币和美元是世界上通用的两种货币之一,写一个程序进行货币间币值转换,其中: 人民币和美元间汇率固定为:1美...
    一抹灵阅读 1,601评论 0 0
  • 联网的通信安全,建立在SSL/TLS协议之上。 本文简要介绍SSL/TLS协议的运行机制。文章的重点是设计思想和运...
    杀破魂阅读 600评论 0 3