程式設計師進階之演算法練習:LeetCode專場

騰訊雲加社群發表於2018-11-26

歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~

本文由落影發表

前言

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可能返回11/3可能返回2;
 collection.getRandom();
複製程式碼

題目解析:

插入和移除數字不麻煩,考慮如何在O(1)時間返回一個數字。 容易知道,放在陣列裡面可以,然後隨機返回一個位置可以實現。 增加可以在陣列最末端增加; 刪除陣列中間某個數字時,可以把最末端的數字放到刪除的位置上;

現在的問題是,如何快速找到陣列中該刪除的某個位置; 考慮用hash來實現。 陣列就是vector<pair<int, int> >; first存val,second存出現次數; 再用一個雜湊map,unordered_map<int, vector> 裡面存對應數字出現的位置;

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;
複製程式碼

總結

這5個題目如果都能獨立完成,那麼水平已經可以足以應付國內各大企業的演算法面。 演算法重在勤思多練,埋怨公司出演算法題是沒用的,多花時間準備才是正道。

此文已由作者授權騰訊雲+社群釋出,更多原文請點選

搜尋關注公眾號「雲加社群」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

相關文章