Leetcode刷题总结(5):Top Interview Questions

可能这样还稍微有针对性一点吧。于是从这些题目中随机了。

44

题意

造个正则匹配机。需要处理普通字符、?(任意单个字符)和*(任意多个字符)。

分析

第一反应是造个有限状态自动机,然后转换成确定的有限状态自动机,然后就可以立即得出答案了。不过不用这么麻烦的吧……??(何况我连这个算法叫什么都忘光了,而且这个算法的复杂度很大吧)

直觉上,首先可以消耗掉模式串两端的普通字符和?号。这之后,模式串中就剩下了两端的*和中间的?和普通字符。接下来要做的似乎就是尝试把模式串中的普通字符和?号匹配成原有串的子序列。

……于是这似乎又变成了一个动态规划问题。

f[i][j]:s[0..i]能否被p[0..j]匹配。
f[i][j]的推导:

  • 若p[j]为普通字符,则查看f[i-1][j-1]能否匹配成功,且f[i]==p[j]
  • 若p[j]为*,则若f[k][j-1](k=0..i)中有一个能匹配成功,就能成功
  • 若p[j]为?,则查看f[i-1][j-1]能否匹配成功

初始化时有许多细节,我写到吐血了。总之最后还是做出来了。

代码

class Solution {
public:
    bool isMatch(string s, string p) {        
        int n = s.length(), m = p.length();
        
        if (n == 0) {
            if (m == 0)
                return true;
            for (int i = 0; i < m; i++)
                if (p.substr(i, 1) != "*")
                    return false;
            return true;
        }
        else if (m == 0) {
            return false;
        }
        
        bool f[n][m];
        memset(f, 0, sizeof(f));
        // f[i][j]:s[0..i]能否被p[0..j]匹配
        
        for (int i = 1; i < n; i++)
            if (p.substr(0, 1) == "*")
                f[i][0] = true;
        
        if (p.substr(0, 1) == "*" || p.substr(0, 1) == "?")
            f[0][0] = true;
        else if (p.substr(0, 1) == s.substr(0, 1))
            f[0][0] = true;
        int cnt = 0;  // 被消耗了几个字符
        if (p.substr(0, 1) != "*")
            cnt++;
        for (int i = 1; i < m; i++) {
            if (cnt >= 2) {
                f[0][i] = false;
                continue;
            }
            
            if (p.substr(i, 1) == "*")
                f[0][i] = f[0][i-1];
            else if (p.substr(i, 1) == "?") {
                cnt++;
                if (cnt <= 1)
                    f[0][i] = f[0][i-1];
                else
                    f[0][i] = false;
            }
            else {
                cnt++;
                if (cnt <= 1) {
                    if (p.substr(i, 1) == s.substr(0, 1))
                        f[0][i] = true;
                    else
                        f[0][i] = false;
                }
            }
        }
        
        // cout << "oi" << endl;
        
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {
                if (p.substr(j, 1) == "*") {
                    for (int k = 0; k <= i; k++) {
                        if (f[k][j-1]) {
                            f[i][j] = true;
                            break;
                        }
                    }
                }
                else if (p.substr(j, 1) == "?") {
                    f[i][j] = f[i-1][j-1];
                }
                else {
                    if (f[i-1][j-1] && s.substr(i, 1) == p.substr(j, 1))
                        f[i][j] = true;
                }
            }
        }
        
        return f[n-1][m-1];
    }
};

时间为4.03%。

234

题意

一个单向链表。是否为回文的?

分析

首先可以直接化为vector来做。但是有没有用O(1)空间复杂度的比较聪明一点的做法呢?

其实不是很难,找到链表的中间位置,同时翻转前一半链表。随后比较翻转过的前一半链表和后一半链表。

代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector a;
        ListNode* p = head;
        while (p != NULL) {
            a.push_back(p->val);
            p = p->next;
        }
        
        int n = a.size();
        for (int i = 0; i < n; i++) {
            if (n - i - 1 <= i)
                break;
            if (a[i] != a[n-i-1])
                return false;
        }
        
        return true;
    }
};

时间为87.95%。

380

题意

设计一个数据结构(听起来很像面试题)。要求三种操作,平均O(1)时间:

  • 插入一个元素(同时检查它是否在该结构中)
  • 删除一个元素(同时检查它是否在该结构中)
  • 随机返回一个元素,要求每个元素被选到的概率相同

分析

首先给出我的一种设计。该数据结构由一个hash表和一个链表元素pool(准确的说,是一个数组)构成。hash表就是普通的hash函数,冲突的元素用链表连起来,注意是双向链表,链表的地址用数组下标来代替。

pool的总大小为N,当前已用元素的结尾位置为M,已用元素总个数为n(因为前面可能有删掉的,所以n<=M)。

  • 插入:hash之后,在pool中分配一个新的元素。如果分配之后,M==N,则分配一个大小为2N的内存池,把原来的元素都拷贝过来。
  • 删除:hash之后,在pool中删除相应的元素。如果删除之后,n<=N/2,则分配一个大小为N/2的内存池,将原来的元素压缩之后拷贝过来(双向链表可以做这个用处,当然也可以用单向链表,直接重新插入一遍)
  • 随机:直接在内存池中<=M的位置随机,随机到空的就直接丢掉。由于N/2 <= ,平均随机次数为2.

没有细算,但是如果hash做得好(这点没法保证。。。),分摊复杂度大概能是O(1)。

看了看解答,发现我tm把这件事想得太复杂了。或者说有些设计是完全没有必要的,因为STL会帮我做了。(https://leetcode.com/problems/insert-delete-getrandom-o1/discuss/85422/AC-C++-Solution.-Unordered_map-+-Vector)

总之,就是用一个HashMap(unordered_map)和一个vector。HashMap中存元素->vector中元素位置的映射,vector中存元素。

  • 插入:直接插。
  • 删除:用map找到该元素在vector中的位置。交换vector中的最后一个元素和该元素,修正map中的内容。删除vector中的最后一个元素。
  • 随机:在vector中直接随机。
    好棒棒。

总之,其实还有一个版本:如果允许重复元素怎么办?目前得到的一个答案是,在HashMap里套一层set,set中记录全部重复元素的位置。插入和删除的原理大致相同,随机的原理也差不多。

代码

class RandomizedSet {
private:
    unordered_map mmap;
    vector vec;
    
public:
    /** Initialize your data structure here. */
    RandomizedSet() {
        srand(time(0));
    }
    
    /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
    bool insert(int val) {
        if (mmap.count(val)) {
            return false;
        }
        vec.push_back(val);
        mmap[val] = vec.size() - 1;
        return true;
    }
    
    /** Removes a value from the set. Returns true if the set contained the specified element. */
    bool remove(int val) {
        if (!mmap.count(val)) {
            return false;
        }
        
        if (vec.size() == 1) {
            vec.pop_back();
            mmap.erase(val);
            return true;
        }
        
        int ind = mmap[val];
        mmap[vec[vec.size() - 1]] = ind;
        vec[ind] = vec[vec.size() - 1];
        vec.pop_back();
        mmap.erase(val);
        return true;
    }
    
    /** Get a random element from the set. */
    int getRandom() {
        return vec[rand() % vec.size()];
    }
};

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * bool param_1 = obj.insert(val);
 * bool param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

时间是19.71%。

104

题解

求二叉树的深度。

分析

递归遍历,直接求。

代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
private:
    int depth;
    
    void inTrav(TreeNode* root, int d) {
        if (root == NULL)
            return;
        depth = max(depth, d);
        inTrav(root->left, d+1);
        inTrav(root->right, d+1);
    }
public:
    int maxDepth(TreeNode* root) {
        depth = 0;
        inTrav(root, 1);
        return depth;
    }
};

时间是45.46%。

198

题意

从一个数组中选一些数,两个数不能相邻。问选出的数之和最大是多少。

分析

不就是求数组的奇数项之和大还是偶数项之和大。。。

好吧,不是,有反例。

用动态规划的。

代码

class Solution {
public:
    int rob(vector& nums) {        
        int n = nums.size();
        
        if (n == 0)
            return 0;
        
        int f[n];
        int ans = 0;
        f[0] = nums[0];
        ans = max(ans, f[0]);
        for (int i = 1; i < n; i++) {
            f[i] = 0;
            for (int j = i - 2; j >= 0; j--)
                f[i] = max(f[i], f[j]);
            f[i] += nums[i];
            
            ans = max(ans, f[i]);
        }
        
        return ans;
    }
};

时间是73.93%。

116

题意

有一棵满二叉树。要求把树上的每一层的结点连接起来。

分析

对于每个结点,把它的左子树内每层最右侧的结点和右子树内每层最左侧的结点连接起来。

一种更简单的做法是利用本层的next指针,一层一层地进行连接。厉害了。(https://leetcode.com/problems/populating-next-right-pointers-in-each-node/discuss/37472/A-simple-accepted-solution)

代码

/**
 * Definition for binary tree with next pointer.
 * struct TreeLinkNode {
 *  int val;
 *  TreeLinkNode *left, *right, *next;
 *  TreeLinkNode(int x) : val(x), left(NULL), right(NULL), next(NULL) {}
 * };
 */
class Solution {
    void trav(TreeLinkNode* root) {
        if (root == NULL)
            return;
        TreeLinkNode* left = root->left, *right = root->right;
        while (left != NULL && right != NULL) {
            left->next = right;
            left = left->right;
            right = right->left;
        }
        trav(root->left);
        trav(root->right);
    }
    
public:
    void connect(TreeLinkNode *root) {
        trav(root);
    }
};

时间位于8.82%的位置。

384

题意

设计一个数据结构,可以将其中元素随机打乱或重置。

分析

那么直接存储一份原来的拷贝。重置的方法采用水塘抽样。

代码

private:
    vector a;
    
public:
    Solution(vector nums) {
        a = nums;
        srand(time(0));
    }
    
    /** Resets the array to its original configuration and return it. */
    vector reset() {
        return a;
    }
    
    /** Returns a random shuffling of the array. */
    vector shuffle() {
        vector b(a);
        for (int i = 1; i < b.size(); i++) {
            int x = rand() % (i + 1);
            if (x != i) {
                swap(b[x], b[i]);
            }
        }
        return b;
    }
};

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * vector param_1 = obj.reset();
 * vector param_2 = obj.shuffle();
 */

时间为66.47%。

152

题意

找一个乘积最大的子串。

分析

递推以每个数为结尾的绝对值最大(正负)的两个值。

代码

超时O(N^2)代码

class Solution {
public:
    int maxProduct(vector& nums) {
        int n = nums.size();
        if (n == 0)
            return 0;
        int ans = nums[0];
        for (int i = 0; i < n; i++) {
            int p = nums[i];
            ans = max(ans, p);
            for (int j = i + 1; j < n; j++) {
                p *= nums[j];
                ans = max(ans, p);
            }
        }
        return ans;
    }
};

瞎写的递推

class Solution {
public:
    int maxProduct(vector& nums) {
        int n = nums.size();
        if (n == 0)
            return 0;
        int f[n], g[n];  // f=max g=min
        int ans = nums[0];
        f[0] = g[0] = nums[0];
        for (int j = 1; j < n; j++) {
            if (nums[j] == 0) {
                f[j] = g[j] = 0;
            }
            else {
                f[j] = g[j] = nums[j];
                if (nums[j] < 0) {
                    f[j] = max(f[j], g[j-1] * nums[j]);
                    g[j] = min(g[j], f[j-1] * nums[j]);
                }
                else {
                    f[j] = max(f[j], f[j-1] * nums[j]);
                    g[j] = min(g[j], g[j-1] * nums[j]);
                }
            }
            ans = max(ans, f[j]);
        }
        
        return ans;
    }
};

时间为17.86%。

36

题意

一个还没有完成的数独题面。请判断现在这个数独是否合法。

分析

参考http://sudoku.com.au/TheRules.aspx,直接暴力求解好了。

代码

class Solution {
public:
    bool isValidSudoku(vector>& board) {
        int f[10];
        // 行
        for (int i = 0; i < 9; i++) {
            memset(f, 0, sizeof(f));
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.')
                    if (f[board[i][j] - '0'] > 0)
                        return false;
                    else
                        f[board[i][j] - '0']++;
            }
        }
        // 列
        for (int i = 0; i < 9; i++) {
            memset(f, 0, sizeof(f));
            for (int j = 0; j < 9; j++) {
                if (board[j][i] != '.')
                    if (f[board[j][i] - '0'] > 0)
                        return false;
                    else
                        f[board[j][i] - '0']++;
            }
        }
        // 块
        for (int i = 0; i < 9; i += 3) {
            for (int j = 0; j < 9; j += 3) {
                memset(f, 0, sizeof(f));
                for (int k = 0; k < 9; k++) {
                    char x = board[i + k / 3][j + k % 3];
                    if (x != '.')
                        if (f[x - '0'] > 0)
                            return false;
                        else
                            f[x - '0']++;
                }
            }
        }
        return true;
    }
};

时间是63.28%。

287

题意

有一个长度为n+1的整数数组,每个数都在[1, n]之间,只有一个重复数字,可能重复多次。请找出这个数字。要求不修改原数组,使用的为常数空间(不算原来的数组),时间复杂度

分析

显然用O(n^2)可以暴力地做出来,那么有什么比较好的方法呢?结论是某种二分查找。假定区间[left, right]包含了这个重复的数字。将该区间二分,统计数组中在左侧区间的数的个数和在右侧区间的数的个数。某一侧的数的数量一定会多一些,那么重复的数字就在这个区间里。显然,时间复杂度是O(n*log(n))。

另一个非常有趣的O(n)算法是把数组看成一个映射,然后在里面寻找环路,时间复杂度是O(n)。具体可以见http://keithschwarz.com/interesting/code/?dir=find-duplicate。

代码

class Solution {
public:
    int findDuplicate(vector& nums) {
        int n = nums.size();
        if (n == 0) {
            return 0;
        }
        else if (n == 1) {
            return nums[0];
        }
        n--;
        int left = 1, right = n;
        while (left < right) {
            int mid = (left + right) >> 1;
            
            // cout << left << ' ' << mid << ' ' << right << endl;
            
            int cnt = 0;
            for (int i = 0; i < n + 1; i++) {
                if (nums[i] <= mid && nums[i] >= left)
                    cnt++;
            }
            
            if (cnt <= mid - left + 1) {
                left = mid + 1;
            }
            else {
                right = mid;
            }
        }
        
        return left;
    }
};

时间是53.02%。

你可能感兴趣的:(Leetcode刷题总结(5):Top Interview Questions)