30道面试常见的数据结构算法题

注意:

边界条件的判断

取地址符传参的使用

溢出问题,long long

题目来源:https://github.com/ZXZxin/ZXBlog/blob/master/%E5%88%B7%E9%A2%98/InterviewAlgorithm.md

新增:LRU

146. LRU 缓存

class DuelListNode(object):
    def __init__(self, key = 0, val=0):
        self.pre = None
        self.next = None
        self.key = key
        self.val = val

class LRUCache(object):

    def __init__(self, capacity):
        """
        :type capacity: int
        """
        self.cache = {}
        self.head = DuelListNode()
        self.tail = DuelListNode()
        self.head.next = self.tail
        self.tail.pre = self.head
        self.capacity = capacity
        self.size = 0

    def get(self, key):
        """
        :type key: int
        :rtype: int
        """
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self.moveToHead(node)
        return node.val

    def put(self, key, value):
        """
        :type key: int
        :type value: int
        :rtype: None
        """
        if key in self.cache:
            node = self.cache[key]
            node.val = value
            # 这里是移动到头
            self.moveToHead(node)
        else:
            node = DuelListNode(key,value)
            # 注意加入缓存中
            self.cache[key] = node
            # 这里是直接加到头,因为本来不在双向链表中,所以不需要移动
            self.addToHead(node)
            self.size += 1
            if self.size > self.capacity:
                removeNode = self.removeTail()
                self.cache.pop(removeNode.key)
                self.size -= 1
        
    def moveToHead(self,node):
        self.removeNode(node)
        self.addToHead(node)

    def addToHead(self,node):
        node.next = self.head.next
        node.pre = self.head
        self.head.next.pre = node
        self.head.next = node

    def removeNode(self,node):
        node.pre.next = node.next
        node.next.pre = node.pre

    def removeTail(self):
        removeNode = self.tail.pre
        self.removeNode(removeNode)
        return removeNode

01背包

参考:https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html

有 number 件物品和一个容量是 capacity 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

思路:

最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”

dp[i][j]:当前背包容量 j,前 i 个物品最佳组合对应的价值

寻找递推关系式,面对当前商品有两种可能性:

第一,包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即dp(i,j)=dp(i-1,j);

第二,还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即

dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);

#include <vector>
using namespace std;

int find_max(vector<int> w,vector<int> v,int capacity){
    int number = w.size();
    vector<vector<int>> dp(number+1,vector<int>(capacity+1,0));

    for(int i = 1;i <= number;i++){
        for(int j = 1;j <= capacity;j++){
            // 包无法装w[i-1]
            if(w[i-1] < j) dp[i][j] = dp[i-1][j];
            // 包装的进,则结果为不装w[i-1]和装w[i-1]中的最大值
            else dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);
        }
    }

    return dp[number][capacity];
}

上述过程的时间空间复杂度都是O(VN),时间复杂度已经没有优化空间,但是空间复杂度仍然可以优化。 

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main(){
    int number,capacity;
    while(cin >> number >> capacity){
        vector<int> ws,vs;
        int w,v;
        for(int i = 0;i < number;i++){
            cin >> v >> w;
            ws.push_back(w);
            vs.push_back(v);
        }
        // dp[i][j]:当前背包容量 j,前 i 个物品最佳组合对应的价值;
        vector<vector<int>> dp(number+1,vector<int>(capacity+1,0));
        for(int i = 1;i <= number;i++){
            for(int j = 1;j <= capacity;j++){
                if(j < vs[i-1]) dp[i][j] = dp[i-1][j];
                // 注意这里对ws,vs都需要-1
                else dp[i][j] = max(dp[i-1][j],dp[i-1][j-vs[i-1]]+ws[i-1]);
            }
        }
        cout << dp[number][capacity] << endl;
    }
    return 0;
}

初始化的细节问题

01背包有一个很有趣的特点,如果题目要求恰好装满背包/总价值最大,两种问题的解其实就是初始化状态不同。

如果要求恰好装满背包,那么对任意数量的i都不可能存在一种方法使背包为0时装满,所以这时我们初始化为INT_MIN

如果要求总价值最大,那么对于容量为0的背包,我们自然初始化所有值为0,这样其实也对应了求解过程。

完全背包

物品可以多次使用

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main(){
    int number,capacity;
    while(cin >> number >> capacity){
        vector<int> ws,vs;
        int w,v;
        for(int i = 0;i < number;i++){
            cin >> v >> w;
            ws.push_back(w);
            vs.push_back(v);
        }
        // dp[i]:当前背包容量为i时最大价值
        vector<int> dp(capacity+1,0);
        for(int i = 1;i <= capacity;i++){
            for(int j = 0;j < number;j++){
                if(vs[j] > i) continue;
                else dp[i] = max(dp[i],dp[i-vs[j]]+ws[j]);
            }
        }
        cout << dp[capacity] << endl;
    }
    return 0;
}

具体例子:Partition Equal Subset Sum

分割数组,使得分割的两个子数组和相等

从这一点我们首先可以得到两个信息:1. 数组的和为偶数,2.只需要找到一个组合,使其和等于数组和的一半即可。 
确定思路后,我们首先想到的方法可能是回溯法,回溯法解本题是完全可解的,但是问题就在于回溯法的速度过慢,无法AC,那么就一定有更快的解。 
其实想到上面的内容就比较容易往背包上靠了,只不过这个背包问题没有价值信息,更简单一些,我们的目标也不是值最大,只要求装满就OK了,未经优化的代码如下:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(auto &n:nums) sum += n;
        if(sum&1 == 1) return false;
        
        int capacity = sum/2;
        int number = nums.size();
        vector<vector<bool>> dp(number+1,vector<bool>(capacity+1,false));
        for(int i = 0;i <= number;i++) dp[i][0] = true;
        for(int i = 1;i <= number;i++){
            for(int j = 1;j <= capacity;j++){
                if(nums[i-1] > j) dp[i][j] = dp[i-1][j];
                else dp[i][j] = (dp[i-1][j]) || (dp[i-1][j-nums[i-1]]);
            }
        }
        return dp[number][capacity];
    }
};

最长递增子序列

(1)O(N*N)解法

Input: 
[10,9,2,5,3,7,101,18]

Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

思路:经典dp,对于每个数dp记录当前最大递增序列长

对于一个数,往前找所有已经遍历的数,若前面的数小于当前数,则 dp[i] = max(dp[i],dp[j]+1)。这题需要注意dp[n]不一定是我们要返回的结果,我们需要另外使用一个max_num来记录最大值

时间复杂度:O(N*N)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.size() < 2) return nums.size();
        int n = nums.size(),max_num = 1;
        vector<int> dp(n,1);
        for(int i = 1;i < n;i++){
            for(int j = 0;j < i;j++){
                if(nums[j] < nums[i]) dp[i] = max(dp[i],dp[j]+1);
            }
            // 注意需要记录最大数,dp[n]不一定是最大结果!!!!
            max_num = max(max_num,dp[i]);
        }
        return max_num;
    }
}

(2)最长上升子序列解的打印

根据上面的方法可以求得dp数组,我们根据dp数组就可以得到最长上升子序列的解。

  • dp数组中的最大值dp[maxi]表示的是以arr[maxi]结尾的,而且是最长的上升子序列;
  • 我们从maxi往前面找,如果前面的某个dp[i]满足arr[i] < arr[maxi] 且dp[maxi] = dp[i] + 1,就说明这个是我们找最长递增子序列时候取的值,可以作为最长递增子序列的倒数第二个数。
  • 然后依次往前找,可以得到解。

(3)改进:时间复杂度O(N*logN)

dp数组size为nums.size+1,里面用来存放长度 i 序列最小末尾的数,而后我们只需每次二分查找更新dp即可 。

例:

假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。

我们定义一个序列B[1..9],然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN),于是算法的时间复杂度就降低到了O(NlogN)!

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size(),r = 0;
        if(n < 2) return nums.size();
        
        vector<int> dp(n,INT_MIN);
        dp[0] = nums[0];
        for(int i = 1;i < n;i++){
            r = binarySearch(dp,nums[i],r);
        }
        return r+1;
    }
    
    int binarySearch(vector<int> &dp,int num,int r){
        // 若记录的最长序列比当前数小,直接增加长度1,返回
        if(dp[r] < num){
            dp[r+1] = num;
            return r+1;
        }
        
        // 否则去查找第一个大于num的数,把它替换为num即可
        int l = 0,mid,ori_r = r;
        while(l < r){
            mid = (l+r)/2;
            if(dp[mid] < num) l = mid+1;
            else if(dp[mid] > num) r = mid;
            else return ori_r;
        }
        dp[r] = num;
        return ori_r;
    }
};

最长公共子序列LCS

题目

就是输入两个字符串str1str2,输出任意一个最长公共子序列

先来看一下子串、子序列还有公共子序列的概念:

(1)字符子串:指的是字符串中连续的n个字符,如abcdefg中,ab,cde,fg等都属于它的字串。

(2)字符子序列:指的是字符串中不一定连续但先后顺序一致的n个字符,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。

(3)  公共子序列:如果序列C既是序列A的子序列,同时也是序列B的子序列,则称它为序列A和序列B的公共子序列。如对序列 1,3,5,4,2,6,8,7和序列 1,4,8,6,7,5 来说,序列1,8,7是它们的一个公共子序列。

思路

dp经典题

时间复杂度时O(s1* s2),空间也是O(s1* s2)

递推式:

#include <vector>
using namespace std;

int LCS(vector<int> s1,vector<int> s2){
    int n1 = s1.size(),n2 = s2.size();
    vector<vector<int>> dp(n1+1,vector<int>(n2+1,0));
    for(int i = 1;i <= n1;i++){
        for(int j = 1;j <= n2;j++){
            if(s1[i-1] == s2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
            // 左边和上面中大的那一个
            else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
        }
    }
    return dp[n1][n2];
}

打印

当得到完整的DP表之后,我们可以通过倒推来得到相应的子序列。

选定一个方向开始走即可

最长公共子串

输入:

s1: abcdefq

s2: cdefab

输出:

4

与上题其实相同

#include <string>
using namespace std;

int longest_substring(string s1,string s2){
    int n1 = s1.size(),n2 = s2.size(),max_len = 0;
    vector<vector<int>> dp(n1+1,vector<int>(n2+1,0));
    for(int i = 1;i <= n1;i++){
        for(int j = 1;j <= n2;j++){
            if(s1[i-1] == s2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
            max_len = max(max_len,dp[i][j]);
        }
    }
    return max_len;
}

int main(){
    string s1 = "abcdefq",s2 = "cdefab";
    cout << longest_substring(s1,s2) << endl;
}

编辑距离(dp)

问题描述

给定 2 个字符串 a, b. 编辑距离是将 a 转换为 b 的最少操作次数,操作只允许如下 3 种:

  1. 插入一个字符,例如:fj -> fxj
  2. 删除一个字符,例如:fxj -> fj
  3. 替换一个字符,例如:jxj -> fyj

思路:

dp

class Solution {
public:
    int minDistance(string word1, string word2) {
        int len1 = word1.size(),len2 = word2.size();
        int dp[len1+1][len2+1] = {0};
        for(int i = 0;i <= len1;i++) dp[i][0] = i;
        for(int i = 1;i <= len2;i++) dp[0][i] = i;
        for(int i = 1;i <= len1;i++){
            for(int j = 1;j <= len2;j++){
                if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
                else dp[i][j] = min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
            }
        }
        return dp[len1][len2];
    }
};

大数相乘

比如算50的阶乘:

  • 我们要先从1开始乘:1*2=2,将2存到a[0]中;
  • 接下来是用a[0]*32*3=6,将6储存在a[0]中;
  • 接下来是用a[0]*46*4=24,是两位数,那么24%10==4存到a[0]中,24/10==2存到a[1]中;
  • 接下来是用a[0]*5a[1]*5+num(如果前一位相乘结果位数是两位数,那么num就等于十位上的那个数字;如果是一位数,num==0);24*5=120,是三位数,那么120%10==0存到a[0]中,120/10%10==2存到a[1]中,120/100==1存到a[2]中;
  • 接下来是用a[0]*6a[1]*6+numa[2]*6+num120*6=720,那么720%10==0存到a[0]中,720/10%10==2存到a[1]中,720/100==7存到a[2]中。。。
#include <string>
using namespace std;

void big_pow(int n){
    int res[20000];
    res[0] = 1;
    int cur,remind = 0,len = 0;
    for(int i = 2;i <= n;i++){
        for(int j = 0;j <= len;j++){
            cur = res[j]*i+remind;
            res[j] = cur%10;
            remind = cur/10;
        }
        while(remind) {
            len++;
            res[len] = remind%10;
            remind /= 10;
        }
    }
    // 最后是反着输出的
    for(int i = len;i >= 0;i--) cout << res[i];
}

int main(){
    big_pow(1000);
}

组合(dfs)

Example:

Input: n = 4, k = 2
Output:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

思路:

这题比较简单,简单dfs即可

class Solution {
public:
    vector<vector<int>> res;
    vector<vector<int>> combine(int n, int k) {
        vector<int> out;
        dfs(n,k,1,out);
        return res;
    }

    void dfs(int n, int k,int start,vector<int> &out){
        if(out.size() == k){
            res.push_back(out);
            return;
        }
        for(int i = start;i <= n;i++){
            out.push_back(i);
            dfs(n,k,i+1,out);
            out.pop_back();
        }
    }
};

全排列(dfs)

无重复数的全排列

Example:

Input: [1,2,3]
Output:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

思路:这题其实是个带visit数组的dfs

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        vector<bool> visit(nums.size(),0);
        vector<int> out;
        vector<vector<int>> res;
        // out存放一次的结果,res存放总结果
        // visit用来记录哪些数被访问过
        dfs(nums,0,visit,out,res);
        return res;
    }
    
    void dfs(vector<int>& nums,int level,vector<bool>visit,vector<int> &out,vector<vector<int>> &res){
        if(level == nums.size()){ res.push_back(out); return;} 
        
        for(int i = 0; i < nums.size();i++){
            // 上一层中已被访问,剪枝跳过
            if(visit[i]) continue;
            visit[i] = 1;
            out.push_back(nums[i]);
            dfs(nums,level+1,visit,out,res);
            out.pop_back();
            visit[i] = 0;
        }
    }
};

无重复数的全排列

Example:

Input: [1,1,2]
Output:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

思路:

有重复时必须先排序

构建如图所示的结果空间树

红色叉的部分表示剪枝。从上述图来看,或者称为森林还恰当一些,先不管这个。这里关键要理解一点,如何去重?比如第三层的后面两个1是如何去掉的?

(1)保证不重复使用数字:使用visit数组

由于递归的for都是从0开始,难免会重复遍历到数字,而全排列不能重复使用数字,意思是每个nums中的数字在全排列中只能使用一次(当然这并不妨碍nums中存在重复数字)。不能重复使用数字就靠visited数组来保证,这就是第一个if剪枝的意义所在。

if(visit[i]) continue;


(2)去重:排序做判断

关键来看第二个if剪枝的意义,这里说当前数字和前一个数字相同,且前一个数字的visited值为0的时候,必须跳过。这里的前一个数visited值为0,并不代表前一个数字没有被处理过,而是递归结束后恢复状态时将visited值重置为0了

if(i > 0 && nums[i]==nums[i-1] && !visit[i-1]) continue;
class Solution {
public:
    vector<vector<int>> res;

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        // 必须先排序,才能判重复
        sort(nums.begin(),nums.end());
        vector<int> out;
        vector<bool> visit(nums.size(),false);
        dfs(nums,visit,out);
        return res;
    }

    void dfs(vector<int>& nums,vector<bool>& visit,vector<int> &out){
        if(out.size() == nums.size()){
            res.push_back(out);
            return;
        }

        for(int i = 0;i < visit.size();i++){
            if(visit[i]) continue;
            // 这里只有visit[i-1]=0才能代表visit[i-1]在这一层被访问过
            // 如果visit[i-1]=1,它是在上面层被访问的,这层里visit[i]是可以被访问的
            if(i > 0 && nums[i-1] == nums[i] && !visit[i-1]) continue;

            out.push_back(nums[i]);
            visit[i] = true;
            dfs(nums,visit,out);
            visit[i] = false;
            out.pop_back();
        }
    }
};

产生子集(递归)

无重复

Example:

Input: nums = [1,2,3]
Output:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

思路:每次在上一次产生的后面加入新数字

1

2

12

3

13

23

123

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> res;
        for(int i = 0;i < nums.size();i++){
            res = build_sub(res,nums[i]);
        }
        return res;
    }
    
    vector<vector<int>> build_sub(vector<vector<int>> &res,int num){
        vector<int> cur;
        auto res_size = res.size();
        for(int i = 0;i < res_size;i++){
            cur = res[i];
            cur.push_back(num);
            res.push_back(cur);
        }
        return res;
    }
};

有重复

Example:

Input: [1,2,2]
Output:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

思路:先排序

1

加入第一个2

2

12

相同的数,第二个2在上一次基础上加入

22

122

思路:

避免相同数造成重复:排序

不同的数,是在所有现有的res后面添加一个数

相同的数,第二个数开始,每次只在上一次生成的数后面添加,从而避免重复

class Solution {
public:
    vector<vector<int>> res;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        res.push_back({});
        for(int i = 0;i < nums.size();i++){
            int cnt = 1;
            while(i < nums.size()-1 && nums[i] == nums[i+1]) {i++;cnt++;}
            getsub(nums,nums[i],cnt);
        }
        return res;
    }

    void getsub(vector<int>& nums,int add_num,int cnt){
        // 记录原始res大小
        int res_size = res.size();
        for(int i = 0;i < res_size;i++){
            vector<int> cur = res[i];
            // 相同数,每次只在上一次添加的里加
            for(int j = 0;j < cnt;j++){
                cur.push_back(add_num);
                res.push_back(cur);
            }
        }
    }
};

N皇后(dfs)

以行为单位,进行递归

对每一行,每一个位置检查放置的合法性

class Solution {
public:
    vector<vector<string>> res;
    vector<vector<string>> solveNQueens(int n) {
        vector<string> out(n,string(n,'.'));
        dfs(0,n,out);
        return res;
    }

    void dfs(int i,int n,vector<string> out){
        if(i >= n) {res.push_back(out);return;}
        for(int j = 0;j < n;j++){
            if(isValid(out,i,j)){
                out[i][j] = 'Q';
                dfs(i+1,n,out);
                out[i][j] = '.';
            }
        }
    }
    
    bool isValid(vector<string> &out,int i,int j){
        int n = out.size();
        for(int k = 0;k < i;k++){
            if(out[k][j] == 'Q' || (j-i+k >= 0 && out[k][j-i+k] == 'Q') || (j+i-k < n && out[k][j+i-k] == 'Q') ){
                return false;
            }
        }
        return true;
    }
};

加速:

用一个一位数组来存放当前皇后的状态。假设数组为int state[n], state[i]表示第 i 行皇后所在的列。那么在新的一行 k 放置一个皇后后:

判断列是否冲突,只需要看state数组中state[0…k-1] 是否有和state[k]相等;
判断对角线是否冲突:如果两个皇后在同一对角线,那么|row1-row2| = |column1 - column2|,(row1,column1),(row2,column2)分别为冲突的两个皇后的位置
 

字典树(判断字符串在不在一个大集合里的时候使用)

使用场景:

在一个大集合里查找每个word在不在里面,时间非常的长。这里其实有个trick,当ab找不到时,abc也找不到,应该进行剪枝。这里通过构建字典树(前缀树)来进行剪枝

定义:

一个保存了8个键的trie结构,"A", "to", "tea", "ted", "ten", "i", "in", and "inn".如下图所示:

字典树主要有如下三点性质:

1. 根节点不包含字符,除根节点意外每个节点只包含一个字符。

2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。

3. 每个节点的所有子节点包含的字符串不相同。

字母树的插入(Insert)、删除( Delete)和查找(Find)都非常简单,用一个一重循环即可,即第i 次循环找到前i 个字母所对应的子树,然后进行相应的操作。实现这棵字母树,我们用最常见的数组保存(静态开辟内存)即可,当然也可以开动态的指针类型(动态开辟内存)。至于结点对儿子的指向,一般有三种方法:

1、对每个结点开一个字母集大小的数组,对应的下标是儿子所表示的字母,内容则是这个儿子对应在大数组上的位置,即标号;(就是一个指针数组

2、对每个结点挂一个链表,按一定顺序记录每个儿子是谁;

3、使用左儿子右兄弟表示法记录这棵树。

三种方法,各有特点。第一种易实现,但实际的空间要求较大;第二种,较易实现,空间要求相对较小,但比较费时;第三种,空间要求最小,但相对费时且不易写。

// 字典树节点
struct tree_node{
    string word;
    // 这是一个指针数组,即数组内存放的是指针
    tree_node *child[26];
    tree_node(){
        word = "";
        for(auto &c:child) c = nullptr;
    }
};

// 字典树
class tree{
public:
    tree_node *root;

    // 字典树初始化根节点
    tree(){
        root = new tree_node();
    }
    
    // 插入操作
    void insert(string s){
        tree_node *cur = root;
        for(auto &c:s){
            int idx = c-'a';
            if(cur->child[idx] == nullptr) cur->child[idx] = new tree_node();
            cur = cur->child[idx];
        }
        cur->word = s;
    }
};

一个应用:Word Search II

Given a 2D board and a list of words from the dictionary, find all words in the board.

Each word must be constructed from letters of sequentially adjacent cell, where "adjacent" cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

Example:

Input: 
board = [
  ['o','a','a','n'],
  ['e','t','a','e'],
  ['i','h','k','r'],
  ['i','f','l','v']
]
words = ["oath","pea","eat","rain"]
Output: ["eat","oath"]
class Solution {
public:
    vector<string> res;
    vector<string> findWords(vector<vector<char>> &board, vector<string> &words) {
        if(board.empty() || board[0].empty() || words.empty()) return res;

        // 建立字典树
        tree T;
        for(auto &word:words) T.insert(word);

        // 对棋盘中每个位置,dfs递归
        for(int i = 0;i < board.size();i++){
            for(int j = 0;j < board[0].size();j++){
                dfs(i,j,board,T.root);
            }
        }
        return res;
    }

    void dfs(int i, int j,vector<vector<char>> &board,tree_node *root){
        auto row = board.size(),col = board[0].size();
        
        // 非法位置判断
        if(i < 0 || i >= row || j < 0 || j >= col || board[i][j] == '$') return;
        // 不存在该字符,直接返回
        int idx = board[i][j]-'a';       
        root = root->child[idx];
        if(!root) return;
        
        // 如果构成一个词,就入栈
        if(!root->word.empty()){
            res.push_back(root->word);
            // 主要清空!!!词表中一个词找到后,下次不能再被重复找到
            // 如[a,a],[a]这种情况
            root->word = "";
        }
        // 这里防止board里的一个字母被反复的在构成一个词里用到
        char store = board[i][j];
        board[i][j] = '$';
        dfs(i-1,j,board,root);
        dfs(i+1,j,board,root);
        dfs(i,j+1,board,root);
        dfs(i,j-1,board,root);
        // 还原回去,下一个词仍可以使用这个字母
        board[i][j] = store;
    }
};

并查集

并查集很实用,实现也非常简单,建议看:这篇博客 还有 这篇

主要可以解决的问题如:

Friend Circles

class Solution {
public:
    int findCircleNum(vector<vector<int>>& M) {
        num = M.size();
        UnionSet();
        for(int i = 0;i < num;i++){
            for(int j = 0;j < num;j++){
                if(M[i][j] == 1){
                    Union(i,j);
                }
            }
        }
        return getSubsetNum();
    }
    
private:
    // 并查集
    int num;
    int *parent = new int [100000];
    
    void UnionSet(){
        /** 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合 */
        for(int i = 0;i < num;i++){
            parent[i] = i;
        }
    }
    
    // 查找过程, 查找元素p所对应的集合编号
    int find(int p){
        while(parent[p] != p){
            // 下面是原始并查集实现            
            // p = parent[p];
            // 路径压缩:这是对并查集的优化,使得其尽量不深,优化搜索效率
            // 路径压缩策略,即当调用一次find(x)时,顺带将x指向根节点;
            p = parent[parent[p]];
        }
        return p;
    }
    
    // 合并元素a和元素b所属的集合
    void Union(int a,int b){
        int aRoot = find(a);
        int bRoot = find(b);
        if (aRoot == bRoot) return;
        parent[aRoot] = bRoot;
    }
    
    // 获得总集合个数
    int getSubsetNum(){
        int cnt = 0;
        for(int i = 0;i < num;i++){
            if(parent[i] == i) cnt++;
        }
        return cnt;
    }
};

总结 - 线段树问题解决的框架

  • 如果问题带有区间操作,或者可以转化成区间操作,可以尝试往线段树方向考虑

    • 从面试官给的题目中抽象问题,将问题转化成一列区间操作,注意这步很关键
  • 当我们分析出问题是一些列区间操作的时候

    • 对区间的一个点的值进行修改
    • 对区间的一段值进行统一的修改
    • 询问区间的和
    • 询问区间的最大值、最小值
      我们都可以采用线段树来解决这个问题
  • 套用我们前面讲到的经典步骤和写法,即可在面试中完美的解决这些题目!

什么情况下,无法使用线段树?
如果我们删除或者增加区间中的元素,那么区间的大小将发生变化,此时是无法使用线段树解决这种问题的。

树状数组

Range Sum Query - Mutable(区间求和问题)

Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.

The update(i, val) function modifies nums by updating the element at index i to val.

Example:

Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8

题意:

题意大致是,给你提供一个二维数组,然后求指定下标之间的数之和,已知数组中的值可以更新,并且更新和求和操作会被频繁调用。最原始的想法就是暴力遍历求和,不过想也不用想,当求和操作操作频繁调用,会超出给定时限。 并且,数组中的值会被频繁更新,我了解到的解决方法有Segement Tree(线段树),Binary Indexed Tree(树状数组) ,和平方根分解三种办法。

树状数组(查询和修改复杂度均为O(logn))

树状数组的优点:

  1. 代码短小,实现简单;
  2. 容易扩展到高纬度的数据;

缺点:

  1. 只能用于求和,不能求最大/小值;
  2. 不能动态插入;
  3. 数据多时,空间压力大;

树状数组:所有的奇数位置的数字和原数组对应位置的相同,偶数位置是原数组若干位置之和,假如原数组A(a1, a2, a3, a4 ...),和其对应的树状数组C(c1, c2, c3, c4 ...)有如下关系:

è¿éåå¾çæè¿°

那么是如何确定某个位置到底是有几个数组成的呢,原来是根据坐标的最低位Low Bit来决定的,所谓的最低位,就是二进制数的最右边的一个1开始,加上后面的0(如果有的话)组成的数字,例如1到8的最低位如下面所示:

坐标          二进制          最低位

1               0001          1

2               0010          2

3               0011          1

4               0100          4

5               0101          1

6               0110          2

7               0111          1

8               1000          8

1.关于lowbit(i & (-i))

其原理是:

i因式分解为2的多少次方:其实就是将 i 转成二进制,二进制最后一个1后面0的个数(这个很巧妙)

求负数的补码(原码取反再加一)的简便方法:把这个数的二进制写出来,然后从右向左找到第一个1(这个1就是我们要求的结果,但是现在表示不出来,后来的操作就是让这个1能表示出来),这个1不要动和这个1右边的二进制不变,左边的二进制依次取反,这样就求出的一个数的补码,说这个方法主要是让我们理解一个负数的补码在二进制上的特征,然后我们把这个负数对应的正数与该负数与运算一下,由于这个1的左边的二进制与正数的原码对应的部分是相反的,所以相与一定都为0,;由于这个1和这个1右边的二进制都是不变的,因此,相与后还是原来的样子,故,这样搞出来的结果就是lowbit(x)的结果。

记得实现的时候:(j & -j)去往后更新,往前累加即可


以下写法参考:https://www.cnblogs.com/grandyang/p/4985506.html

class NumArray {
public:
    NumArray(vector<int> nums) {
        // 存放原始的数组
        data.resize(nums.size());
        // 树状数组,0不放置,只是padding用
        bit.resize(nums.size()+1);
        // 建立原始树状数组
        for(int i = 0;i < nums.size();i++){
            update(i,nums[i]);
        }
    }
    
    // 这里是从该位置开始往后更新
    void update(int i, int val) {
        int add = val - data[i]; 
        // 这里是j=i+1,因为0位置padding用
        for(int j = i+1;j < bit.size();j += (j&-j)){
            bit[j] += add;
        }
        data[i] = val;
    }
    
    int sumRange(int i, int j) {
        return getSum(j+1)-getSum(i);
    }
    
    // 利用树状数组计算1-num区间内的和
    // 从后往前加
    int getSum(int num){
        int res = 0;
        for(int i = num;i > 0;i -= (i&(-i))){
            res += bit[i];
        }
        return res;
    }
    
private:
    vector<int> data,bit;
};

线段树

线段树实现与应用:Segment Tree 线段树 原理及实现 - 简书(写的非常清楚,线段树不光能区间求和,还能求区间最小最大值)

线段树更加容易理解,上题解法:

#include <vector>
#include <iostream>
using namespace std;

// 线段树节点
struct segTreeNode{
    int begin,end,val;
    segTreeNode *left,*right;
    segTreeNode(int x,int y,int v){
        begin = x;
        end = y;
        val = v;
        left = NULL;
        right = NULL;
    }
};


class NumArray {
public:
    NumArray(vector<int>& n) {
        nums = n;
        root = structTree(0,nums.size()-1);
    }

    void update(int i, int val) {
        segTreeNode *cur = root;
        int add = val-nums[i];
        nums[i] = val;
        while(cur){
            cur->val += add;
            int mid = (cur->begin+cur->end)/2;
            if(i <= mid) cur = cur->left;
            else cur = cur->right;

        }
    }

    int sumRange(int i, int j) {
        return getSum(root,i,j);
    }

private:
    vector<int> nums;
    segTreeNode *root;

    segTreeNode * structTree(int begin,int end){
        if(begin > end) return NULL;

        segTreeNode *root = new segTreeNode(begin,end,nums[begin]);
        if(begin == end) return root;

        int mid = (begin+end)/2;
        root->left = structTree(begin,mid);
        root->right = structTree(mid+1,end);
        root->val = root->left->val+root->right->val;
        return root;
    }

    int getSum(segTreeNode * root,int i,int j){
        if(root->begin == i && root->end == j) return root->val;

        segTreeNode *cur = root;
        int mid = (cur->begin+cur->end)/2;
        if(mid < i) return getSum(cur->right,i,j);
        else if(mid >= j) return getSum(cur->left,i,j);
        else return getSum(cur->left,i,mid)+getSum(cur->right,mid+1,j);
    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * obj->update(i,val);
 * int param_2 = obj->sumRange(i,j);
 */

KMP算法

这篇博客 和 这篇

#include <vector>
#include <iostream>
using namespace std;

// next[i]的含义:
// 在str[i]之前的字符串str[0...i-1]中,必须以str[i-1]结尾的后缀子串(不能包含str[0])
// 与必须以str[0]开头的前缀子串(不能包含str[i-1])的最大匹配长度;
vector<int> getNext(string s){
    int n = s.size(),cnt = 0;
    vector<int> next(n,0);
    next[0] = -1;

    // 注意这里不自动+1
    for(int i = 2;i < n;){
        // 相同时
        if(s[cnt] == s[i-1]) next[i++] = ++cnt;
        else{
            // 不同时,往前找cnt位置的最大匹配
            if(cnt > 0) cnt = next[cnt];
            else next[i++] = 0;
        }
    }
    return next;
}

bool kmp(string s,string pattern){
    if(pattern.empty()) return true;
    if(s.empty()) return false;

    vector<int> next = getNext(pattern);
    int i = 0,j = 0;
    while(i < s.size() && j < pattern.size()){
        if(s[i] == pattern[j]){
            i++;j++;
        }else{
            if(next[j] == -1){
                i++;
            }
            else j = next[j];
        }
    }
    return j == pattern.size()? true:false;
}


int main(){
    string s1 = "abcdeabcdabcdefghij",s2 = "abcdef";
    cout << kmp(s1,s2) << endl;
}

最大公约数&最小公倍数

最大公约数:https://www.cnblogs.com/fogwind/p/6092845.html

// 辗转相除法
int gcd(int a,int b){
    if(b == 0) return a;
    return gcd(b,a%b);
}

int main(){
    int res = gcd(319,377);
    cout << res << endl;
}

最小公倍数:求最大公约数和最小公倍数的算法_烂烂三三的博客-CSDN博客

// 最大公倍数:辗转相除法
int gcd(int a,int b){
    if(b == 0) return a;
    return gcd(b,a%b);
}

// 最小公约数:先求出最大公约数。用两个数的乘积除以最大公约数即可
int lcm(int a,int b){
    return a*b/gcd(a,b);
}

int main(){
    int res = lcm(6,9);
    cout << res << endl;
}

生成素数

// 普通筛法
vector<int> getPrimes(int max){
    vector<int> primes;
    vector<bool> is_prime(max+1,true);
    is_prime[0] = false;
    is_prime[1] = false;
    for(int i = 2;i <= max;i++){
        bool flag = true;
        // 对每个可能取值,判断2到sqrt(i)之间,是否有能整除它的
        for(int j = 2;j*j <= i;j++){
            if(i % j == 0){
                // 有,则不是素数
                is_prime[j] = false;
                flag = false;
                break;
            }
        }
        if(flag) primes.push_back(i);
    }
    for(auto &i:primes) cout << i << ' ';
    cout << endl;
    return primes;
}

vector<int> getPrimes(int max){
    vector<int> primes;
    vector<bool> is_prime(max+1,true);
    is_prime[0] = false;
    is_prime[1] = false;
    for(int i = 2;i <= max;i++){
        if(is_prime[i]){
            primes.push_back(i);
            // 更新is_prime记录,这步很关键,把i的倍数的数都变为false
            for(int j = 2*i;j <= max;j += i){
                is_prime[j] = false;
            }
        }
    }
    return primes;
}

因式分解(生成一定范围内素数)

思路:这题跟丑数生成有些相似,空间换时间,否则时间复杂度太高

#include <vector>
#include <iostream>
using namespace std;

// 埃式筛法:获得max范围内的素数
vector<int> getPrimes(int max){
    vector<int> primes;
    vector<bool> is_prime(max+1,true);
    is_prime[0] = false;
    is_prime[1] = false;
    for(int i = 2;i <= max;i++){
        if(is_prime[i]){
            primes.push_back(i);
            // 更新is_prime记录,这步很关键,把i的倍数的数都变为false
            for(int j = 2*i;j <= max;j += i){
                is_prime[j] = false;
            }
        }
    }
    return primes;
}

// 组合素数,获得因式分解结果
vector<int> UniqueDecomposition(int n){
    vector<int> primes = getPrimes(n);
    vector<int> res;
    for(int i = 0;i < primes.size();i++){
        int p = primes[i];
        while( n % p == 0){
            res.push_back(p);
            n /= p;
        }
    }
    return res;
}

int main(){
    vector<int> res = UniqueDecomposition(9828);
    for(auto &n:res) cout << n << ' ';
}

乘法快速幂

class Solution {
public:
    double myPow(double x, long long n) {
        if(n == 0) return 1;
        
        int flag = 1;
        bool neg = n < 0? true:false;
        if(n < 0) n = -n;
        
        double res = 1,cur = x;
        while(n){
            if(flag & n) res *= cur;
            cur *= cur;
            n =  n >> 1;
        }
        return neg? 1/res:res;
    }
};

矩阵快速幂

#include <vector>
#include <iostream>
using namespace std;

// 定义矩阵
struct matrix{
    int row;
    int col;
    vector<vector<int>> m;
    matrix(int i,int j){
        row = i;
        col = j;
        m.resize(row,vector<int>(col));
    }
};

// 必须满足A.col = B.row  才能相乘
matrix mul(matrix A,matrix B){
    int row = A.row,col = B.col;
    matrix C(row,col);
    for(int i = 0;i < row;i++){
        for(int j = 0;j < col;j++){
            for(int k = 0;k < col;k++){
                C.m[i][j] += (A.m[i][k]*B.m[k][j]);
            }
        }
    }
    return C;
}

// A必须为方阵才能有幂
matrix pow(matrix A,int n){
    matrix res(A.row,A.col);
    // 初始化为单位矩阵
    for(int i = 0;i < A.row;i++){
        res.m[i][i] = 1;
    }
    while(n > 0){
        if(n & 1) res = mul(res,A);
        A = mul(A,A);
        n = n >> 1;
    }
    return res;
}

int main(){
    matrix A(2,2);
    A.m = {{1,2},{3,4}};
    matrix res = pow(A,2);
    for(int i = 0;i < 2;i++){
        for(int j = 0;j < 2;j++){
            cout << res.m[i][j] << ' ';
        }
        cout << endl;
    }
}
  • 5
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
算法数据结构它们分别涵盖了以下主要内容: 数据结构(Data Structures): 逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问的步骤形式化为一系列指令,使得计算机可以执行以求解问算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值