面试刷题总结

链表

  • 部分操作需要设置虚拟头指针来方便循环
  • 快慢指针

反转链表

  • 通过双指针来控制链表的逆置,关键在于指针的初始化和循环中逆置的方式。需要用两个指针分别指向当前节点及前驱。
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre=NULL;
        ListNode* cur=head;
        while(cur){             //当前指针指向不为空时即,还存在需要逆置的结点时
            ListNode* temp=cur->next;       //存储原本的后继结点,防止因改变指针丢失
            cur->next=pre;
            pre=cur;
            cur=temp;

        }
        return pre;

    }
};
  • 根据双指针法写递归时,递归入口参数即为while循环中更改的两个指针,主函数中传入的参数即为双指针初始化形式,而递归退出条件即为链表遍历结束。
class Solution {
public:
    ListNode* reverse(ListNode *pre,ListNode* cur){
        if(cur==NULL) return pre;
        ListNode* temp=cur->next;
        cur->next=pre;   
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        
        return reverse(NULL,head);

    }
};

环形链表思路非常重要

讲解

哈希表

基础知识

  • 三种哈希方法:数组,set,map
  • 当我们要使用集合时,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要有序,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset
  • 当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要想到哈希法。

字母异位词

242
49

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        multiset<int> flag(nums1.begin(),nums1.end());		//允许Key重复出现因此使用Multiset
        vector<int> res;
        for(auto p:nums2){			//遍历数组Nums2
            if(flag.find(p)!=flag.end()){					//set关联容器的查找操作
                res.push_back(p);
                flag.erase(flag.find(p));					//关联容器的删除操作
            }
        }
        return res;
    }
};
  • set的查找操作set.find()未找到则返回end,找到返回迭代器
  • set的删除操作set.erase(x)。有两种重载函数,第一种删除set中所有x的元素。第二种传入迭代器x,仅删除指定位置的元素

滑动窗口

  • 适合用于求连续数组的符合规则的子数组,外层for循环右指针添加元素,内层while循环左指针删除元素。

求最小连续窗口

模板

for (右指针在给定数组范围内循环) {
        将元素加入滑动窗口;
        窗口内计数++while (滑动窗口内符合最小规则) {
            min最小值和滑动窗口长度;
            收缩滑动窗口;
        }
    }

长度最小的子数组

最小覆盖子串

class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> dirs, dirt;          //记录
        for (int i = 0; i < t.size(); i++) {
            dirt[t[i]]++;
        }
        int i = 0;
        string res;
        int cnt = 0;
        for (int j = 0; j < s.size(); j++) {
            dirs[s[j]]++;
            if (dirs[s[j]] <= dirt[s[j]])  cnt++;       //是一个新的元素
            while (dirs[s[i]] > dirt[s[i]]) {           //收缩窗口
                dirs[s[i++]]--;
            }
            if (cnt==t.size()) {                        //窗口内满足条件
                //再取最小子串
                if (res.empty() || res.size() > j - i + 1) {
                    res = s.substr(i, j - i + 1);
                }
            }
        }
        return res;
    }
};
  • 使用哈希表统计字母出现情况
  • 且在加入元素后需要统计是否满足模式串的内容

求最大连续窗口

模板

for (右指针在给定数组范围内循环) {
    if (是否算作新元素) {
        规定++;
        将元素加入滑动窗口;
    }
    while (滑动窗口内不符合规则) {        
        收缩滑动窗口
    }
    max取最大值和滑动窗口长度
}

水果成篮

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int ,int> box;        //哈希表 无序 不能重复 不可修改
        int cnt=0;                  //记录篮子中的种类数目
        int i=0;
        int bodary=0;
        for(int j=0;j<fruits.size();j++){
            if(box[fruits[j]]++==0){        //该元素是一个新元素
                cnt++;
            }
            while(cnt>2){               //篮子内不止两种元素时 收缩窗口
                box[fruits[i]]--;        
                if(box[fruits[i]]==0){  //窗口收缩完成
                    cnt--;
                }
                i++;
            }
            bodary=max(bodary,j-i+1);   //滑动窗口内取最大值
        }

        return bodary;
    }
};

双指针法

  • 思路与滑动窗口中有很多重复,两者可以结合使用
    通过前后两个指针向中间逼近,在一个for循环下完成两个for循环的工作。

N数之和问题

  • 将指针与滑动窗口放缩思想结合更容易理解
  • 基础为三数之和,3+数添加一层for循环即可

三数之和

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(),nums.end());
        for(int i=0;i<nums.size();i++){
            if(nums[i]>0){		//剪枝优化可略去
                return res;
            }

            if(i>0&&nums[i]==nums[i-1]){		//去重
                continue;
            }
            int left=i+1;
            int right=nums.size()-1;
            while(right>left){					//滑动窗口,在该窗口内判断条件
                if(nums[i]+nums[left]+nums[right]>0){
                    right--;
                }else if(nums[i]+nums[left]+nums[right]<0){
                    left++;
                }else {
                    res.push_back(vector<int>{nums[i],nums[left],nums[right]});
                    //两个while循环均用来去重
                    while(right>left&&nums[right]==nums[right-1]){
                        right--;
                    }
                    while(right>left&&nums[left]==nums[left+1]){
                        left++;
                    }
                    right--;
                    left++;
                }
            }
        }
        return res;
    }
};

四数之和

//多添加一层循环加一个指针,之后继续按照三数之和来讨论,以此类推
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); i++) {
        	//还可以剪枝优化
            if (i > 0 && nums[i] == nums[i - 1]) {      //a去重
                continue;
            }
            //转换成三数之和的形式
            auto temp = target - nums[i];
            for (int j = i + 1; j < nums.size(); j++) {
                if (j!=i+1&&nums[j] == nums[j - 1]) {      //a去重
                    continue;
                }
                int left = j + 1;
                int right = nums.size() - 1;
                while (right > left) {
                    if ((long)nums[j] + nums[left] + nums[right] > temp) {
                        right--;
                    }
                    else if ((long)nums[j] + nums[left] + nums[right] < temp) {
                        left++;
                    }
                    else {
                        res.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right] });
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left+1]) left++;
                        left++;
                        right--;
                    }                   
                }
            }            
        }
        return res;
    }
};

模拟

  • 控制每次循环的量不变,如区间的选择,每次应保持一致找出规律

字符串处理

  • 使用 getline(cin,s) 函数来获取字符串时,仅当输入换行符才会停止
  • 当在处理时,规定条件可逆向思考。如:要求删除指定条件可转化为保留满足条件的字符串;

字符串反转

  • 各种反转旋转都是基于一定规则的reverse函数下的整体翻转和局部翻转的结合
    翻转1
    翻转2
    翻转3

KMP算法

  • 通过计算部分匹配值构造Next数组

大小写转换

  • 使用#include <cctype>函数库包含着 isupper(c) islower (c)函数判断字符是否为大小写,然后通过c=toupper(c) 或 c= tolower(c)来转换大小写
  • 判断语句分类讨论

子串输出

  • 使用函数string& insert(int pos, int n, char c); //在指定位置插入n个字符c
  • string substr(int pos = 0, int n = npos) const; //返回由pos开始的n个字符组成的字符串

进制转换

  • 从后向前遍历,根据字符串的值和权值相乘来累加,需要用到#include <cctype>库的大小写判断,以及#include <cmath>库的pow函数
  • 以及一种新的标准输出函数while(cin >> hex >> res) //hex表示读入十六进制数 cout << dec << res << endl; //dec表示输出十进制数

字符串分类问题

错误记录

  • 使用了string查找函数rfind(c) 从后向前查找c的位置
  • map<string,int> p来方便管理关键字与键值的关系。其中的直接使用字符串下标来访问以及存储语法非常好用,需要学会
  • deq<string> q的双端数组来限定所存储元素个数

字符串转换

  • 前两步合并与提取很简单
  • 第三步的转换采用了map容器,列出字典来转化相应字符。其中使用了关联容器的p.find(x)函数来在map中查找x元素,若不存在则返回p.end()该函数主要用在查找map中是否含有某元素的关系且不需要插入新的元素的情况。若使用下标来查找,会自动插入新元素

字符串排序

按指定规则来排序字母

  • 本想重载sort函数中的排序方法函数,但能力不足想不出来,唉
  • 双层循环,外层基于字母排序遍历26个字母,内层遍历字符串。此时可以保证相同英文字母的稳定排序,之后将排好序的字母插入新的字符串。最后重新遍历原字符串,遇到字母输出新字符串内容,否则输出原字符串。这是一种用空间换时间的方法。

回溯算法

参考文章

  • 通过递归,将大问题划分为小问题后,再通过回溯(类似出栈操作)来尝试同一前置类下的其他序列。解决组合、切割、子集、排列、棋盘问题
  • 注意可以剪枝优化
    模板
    void backtracking(参数) {
        if (终止条件) {
            存放结果;
            return;
        }
        for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
            处理节点;		//判断该种组合是否符合题目要求
            backtracking(路径,选择列表); // 将该种组合向下递归
            回溯,撤销处理结果			 //同时撤
        }
    }

组合问题

数字组合

class Solution {
public:
    vector<vector<int>> res;
    vector<int>  path;          //已经遍历的路径
    void backtracking(int n, int k,int Index) {			//k为需要遍历的层数
        if (k == path.size()) {
            res.push_back(path);
            return;
        }
        for (int i = Index; i <= n; i++) {
            path.push_back(i);
            backtracking(n, k , i + 1);  //向下层遍历
            path.pop_back();
        }
    }

    vector<vector<int>> combine(int n, int k) {
        res.clear();
        backtracking(n, k, 1);
        return res;
    }
};
  • 同样看作树

电话号码的组合

class Solution {
public:
    vector<string> result;
    string temp;
    void backtracking(vector<string> p, int high) {
        if (high == p.size()) {
            result.push_back(temp);
            return;
        }
        for (int i = 0; i < p[high].size(); i++) {
            temp.push_back(p[high][i]);     //在组合结果中插入新的字符
            backtracking(p, high + 1);      //递归下一层
            temp.pop_back();       //回溯
        }

    }
    vector<string> letterCombinations(string digits) {
        vector<string> p;
        vector<string> dir = { "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        for (int i = 0; i < digits.size(); i++) {           //将数字转化成字符串集合
            p.push_back(dir[digits[i]-'2']);
        }
        result.clear();
        if (digits.size() == 0) {                           //空串处理
            return result;
        }
        backtracking(p, 0);
        return result;
    }
};

组合总和

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;

    void backtracking(vector<int>& candidates,int index,int target,int sum){
        if(sum==target){
            res.push_back(path);
            return;
        }
        for(int i=index;i<candidates.size()&&sum+candidates[i]<=target;i++){		//剪枝优化
            sum+=candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,i,target,sum);
            sum-=candidates[i];
            path.pop_back();
        }

    }


    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        res.clear();
        sort(candidates.begin(), candidates.end()); // for循环中加入提前判断语句,则需要排序
        backtracking(candidates,0,target,0);
        return res;
    }
};
  • 在本题中元素可以重复则传入下标不用+1,运用排序将所给元素序列升序,在for循环中加入target和sum的判断语句,就可以减少下一层的无用递归

组合加上重复性问题

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;

    void backtracking(vector<int>& candidates,int index,int target,int sum){
        if(sum==target){
            res.push_back(path);
            return;
        }
        for(int i=index;i<candidates.size()&&sum+candidates[i]<=target;i++){		//剪枝优化
            if(i>index&&candidates[i]==candidates[i-1]) continue;                   //同一层不能重复取相同元素
            sum+=candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,i+1,target,sum);
            sum-=candidates[i];
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        res.clear();
        sort(candidates.begin(), candidates.end()); // for循环中加入提前判断语句,则需要排序
        backtracking(candidates,0,target,0);
        return res;
    }
};
  • if(i>index&&candidates[i]==candidates[i-1]) continue; 该行语句来控制组合重复性问题,同一层中相同元素不可以重复选取

切割问题

  • 组合是判断for遍历的下标之前的字符串,而切割是判断下标之后的字符串

分割回文串

class Solution {
public:
    vector<vector<string>> res;
    vector<string> path;
    void backtracking(string s,int index){
        if(index>=s.size()){
            res.push_back(path);
            return;
        }
        for(int i=index;i<s.size();i++){
            if(isValid(s,index,i))  {           //是回文串则在此切割,向下一个字符遍历           
                path.push_back(s.substr(index,i-index+1));
                backtracking(s,i+1);
                path.pop_back();
            }
        }

    }

    bool isValid(string str,int index,int i){           //判断区间index~i之间的字符是不是回文串
        for(;index<i;i--,index++){
            if(str[index]!=str[i]){
                return false;
            }
        }
        return true;
    }
    vector<vector<string>> partition(string s) {
        string temp;
        backtracking(s,0);
        return res;
    }
};

IP地址分割

class Solution {
public:
	vector<string> res;
	string path;
	void backtracking(string s,int index,int k) {
        if(k==4){           //当前已经分割出了四段
		    if (index==s.size()) {      //整个字符串都已经遍历完,则满足所有条件可入结果
			    res.push_back(path);
		    }
            return;
        }
		string temp;
		for (int i = index; i < s.size(); i++) {
			if (isValid(s, index, i)) {             //判断待分割合法性
				temp = s.substr(index, i - index + 1);  
				path+=temp;                                 //插入分割字符
				if (k != 3) {                               //还没分成四份,则加.
					path += '.';
				}
				backtracking(s, i + 1, k + 1);              //继续下层分割
				if (k != 3) {                               //回溯,
					path.pop_back();
				}
				path = path.erase(path.size() - (i - index + 1), i - index + 1);	
			}else{
                break;                                      //当前已经不合法,直接退出
            }
		}

	}

	bool isValid(string s, int index, int i) {			//判断从index——i的字符串是否符合条件
		if (s[index] == '0'&&index!=i) return false;	//前导0的情况
		int sum = 0;
		int mi = 0;
		for (; i >= index; i--,mi++) {
			sum += (s[i] - '0') * pow(10, mi);
		}
		if (sum <= 255) {
			return true;
		}
		else {
			return false;
		}
	}

	vector<string> restoreIpAddresses(string s) {
		res.clear();
		backtracking(s, 0, 0);
		return res;
	}
};
  • 加强版的分割串,注意边界条件以及退出遍历的条件

子集问题

子集

class Solution {
public:
	vector<vector<int>> res;
	vector<int> path;

	void backtracking(vector<int>& nums, int index) {
		res.push_back(path);
		if (index == nums.size()) {		//可省略,for循环中已经保证index<nums.size()
			return;
		}
		for (int i = index; i < nums.size(); i++) {
			path.push_back(nums[i]);
			backtracking(nums, i + 1);
			path.pop_back();
		}

	}
    
	vector<vector<int>> subsets(vector<int>& nums) {
		res.clear();
		backtracking(nums, 0);
		return res;
	}
};
  • 相比于前两种较简单,完全套模板,注意每一层都保存结果即可

子集 II

  • 在以上模板的基础上,添加上面组合中解决重复性问题的判断语句即可

递增子序列

  • 在不对原有序列排序的情况下,解决每一层的重复性问题if(used[nums[i]+100]==1) continue;该题采用标记数组来哈希,还可以使用unorder_set,但占用空间较大

排列问题

全排列

  • 不用控制每一层重复性,而需要控制每一棵子树中不能重复,在参数中传入一个标记数组,标记已经访问过的下标即可

排列中的重复性

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int> nums, vector<bool>& flag) {
        if (nums.size() == path.size()) {
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (flag[i] == true)  continue;
            if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==false) continue;
            flag[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, flag);
            path.pop_back();
            flag[i] = false;
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> flag(nums.size(), false); 
            sort(nums.begin(), nums.end());
        backtracking(nums, flag);
        return res;
    }
};

  • flag[i-1]==false语句代表同一层上,前一位已经使用过了
  • 使用i>0&&nums[i]==nums[i-1]来判断重复性,需要排序

棋盘问题

  • 把棋盘看作一个二维数组,用树来理解,对于一个x*y的棋盘,树的深度就是棋盘的行数,数的宽度就是棋盘的列数,每个结点代表棋盘中选择的每一处

N皇后问题


vector<vector<string>> result;				//p中的每个元素都记录不同的棋盘

void backtracking(vector<string>& board, int n, int row) {			//传入一种棋盘的情况,给定的棋盘行数n,该层递归遍历的棋盘行row
 
    if (row == n) {								//终止条件:遍历到最后一行,即所有类型的组合都已经尝试完
        result.push_back(board);				//在结果中加入该种情况
        return;
    }

    for (int col = 0; col < n; col++) {			//遍历该行中的每一个元素,来尝试不同组合情况
        if (isValid(board, n, row, col)) {
            board[row][col] = 'Q';
            backtracking(board, n, row + 1);	//该处满足情况,可以继续向下层尝试
            board[row][col] = '.';				//回溯该处,并继续for循环防止干扰组合结果
        }
    }
}

bool isValid(vector<string>& board, int n, int row, int col) {			//判断若在棋盘的[row][col]处放上皇后,会出错吗
    //不需要判断同一行是否有,因为在递归中的每一层for循环中,都会回溯使得同一行一定都是.
    //判断同一列
    for (int i = 0; i < n; i++) {
        if (board[i][col] == 'Q') {					//只要该列不存在其他Q
            return false;
        }
    }
    //判断左上角方向
    for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) {		//向棋盘左上方遍历即可
        if (board[i][j] == 'Q') {
            return false;
        }
    }
    //判断右上角方向
    for (int i = row, j = col; i >= 0 && j < n; i--, j++) {		//向棋盘右上方遍历即可
        if (board[i][j] == 'Q') {
            return false;
        }
    }

    return true;

}


哈希排序

  1. 可以采用数组,set容器,map容器来解决查找对应的问题

动态规划

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值