"LeetCode题目思路整理"

  "LeetCode"

Posted by Xu on May 14, 2018

LeetCode刷题思路

LeetCode

主要tag:数组、字符串、链表、树、栈、队列、动态规划、哈希表、二分查找

题目分类:

# Title Solution Difficulty Tag  
1 两个排序数组的中位数 O Hard 数组 | 数据搜索  
5 最长回文子串 O Medium 字符串 | 动态规划  
7 反转整数 O Easy 数学 | 其他  
8 字符串转整数 O Medium 字符串  
10 字符串常规匹配 O Hard 字符串 | 递归 |动态规划  
11 盛最多水的容器 O Medium 数组  
13 罗马数字转整数 O Easy 字符串  
15 三数之和 O Medium 数组 | 两点法  
17 电话号码的字母组合 O Medium 字符串 | 递归  
20 有效的括号 O Easy 字符串 | 回溯法  
21 合并两个有序链表 O Easy 链表 | 优先级队列 | 归并法  
29 两数相除 O Medium 数学 | 位运算  
32 最长有效括号长度 O Hard 动态规划 | 栈  
33 搜索旋转排序数组 O Medium 数组 | 二分查找  
34 搜索范围 O Medium 数组 | 二分查找  
35 搜索插入位置 O Easy 数组 | 二分查找  
36 有效的数读独 O Medium 数组 | 哈希表  
38 报数 O Easy 数学  
39 找到数组中元素和为给定值的所有组合,不限元素使用次数 O Medium 递归  
41 寻找第一个未出现的正数 O Medium 数组 | 哈希表  
42 容器可以装多少水 O Medium 数组 | 动态规划 |两点法  
44 字符串匹配2 O Hard 字符串 | 递归 |动态规划  
48 旋转图形 O Medium 数组 | 递归  
49 字符串分类 O Medium 数组 | 哈希表,索引  
50 求幂Pow(x, n) O Medium 数学 | 位运算  
53 最大子序和 O Easy 数组 | 动态规划  
54 旋转打印矩阵 O Medium 数组 | 递归  
55 跳跃游戏 O Medium 数组 | 其它  
56 区间合并 O Medium 数组 | 其它  
62 机器人走格子路径 O Medium 数组 | 动态规划  
69 求平方根 O Easy 数学 | 边界  
73 设置矩阵0行和列 O Medium 数组 | 哈希表  
75 颜色排序 O Medium 数组 | 两点法  
76 最小匹配窗口 O *Hard 数组 | 哈希表 |其它  
78 求所有可能的子集 O Medium 数组 | 递归  
79 字符串匹配(二维数组中) O Medium 数组 | 递归 |回溯法  
84 求最大面积子矩阵 O *Hard 数组 | 栈  
91 解码方法总数 O Medium 字符串 | 动态规划  
94 二叉树中序遍历 O Medium 树 | 迭代  
98 验证是否是二叉搜索树 O Medium 树 | 迭代  
101 验证是否是对称树 O Easy 树 | 迭代  
102 层次遍历打印 O Medium 树 | 迭代  
103 Zigzag打印 O Medium 树 | 迭代  
104 求树的高度 O Easy  
105 根据前序和中序遍历序列构造树 O Medium  
108 根据递增序列构造查找树 O Easy  
116 层次链表连接 O Medium 树 | 层次遍历  
122 股票交易 O Easy 数组 | 其它  
124 最大路径和 O Hard 树 | 递归  
125 有效回文 O Easy 字符串 | 其它  
127 单词接龙 O *Medium 图 |BFS  
128 最长连续序列长度 O *Hard 数组 | 哈希表  
130 被包围的区域 O Medium 图 |DFS  
131 回文分割 O Medium 字符串 |DFS |递归  
134 汽油站 O *Medium 数组 | 贪心算法 |其它  
138 带有随机指针的链表复制 O *Medium 链表  
139 单词分割I(II) O *Medium 字符串| 动态规划  
146 LRU Cache O *Hard 链表 | 哈希表  
148 链表排序 O Medium 链表  
149 求在一条线上点的最大个数 O Hard 数组 |其他  
150 计算逆波兰表达式 O Hard 数组 | 数据结构 |栈  
152 最大乘积子串 O Medium 数组  
162 找出”山顶”元素 O Medium 二分查找  
166 计算循环小数的分数值 O Medium 其它  
169 求数组中的主元素 O *Medium 数组 | 其它  
171 Excel列号转换 O Easy 字符串 | 其它  
172 n的阶乘尾部0的个数 O Easy 数学  
179 将所有数字拼成一个最大数 O Medium 数学 | 数组  
189 循环移动数组 O Easy 数组 | 其它  
190 翻转bits O Easy 位运算  
198 入室抢劫 O Easy 动态规划  
200 岛屿的数量 O Medium 递归 | DFS  
202 快乐数 O Easy 数学  
207 课程安排 O Medium 递归 | DFS | 拓扑排序  
210 课程安排II O Medium 递归 | DFS | 拓扑排序  
215 找到第k大的元素 O Medium 数组 | 排序 |搜索  
217 是否存在重复元素 O Easy 数组 | 哈希  
227 基础计算器II O Medium 字符串 | 数学 | 后缀表达式  
230 二叉树中给出第k大的元素 O Medium 树 | 迭代 | 后缀表达式  
234 回文链表 O Medium 链表  
236 求两个节点的第一个公共祖先 O Medium 树 | 迭代  
237 删除链表给定节点 O Easy 链表  
238 剩余数组的乘积 O Medium 数组 | 动态规划  
239 滑动窗口最大值 O Hard 数组 | 双端队列 | 数据结构  
240 二位数组搜索指定数值 O Medium 数组 | 数据搜索  
268 缺失数字 O Easy 数组 | 位运算  
279 完全平方和 O Medium 动态规划  
283 移动0元素 O Easy 其它  
287 找到重复数字 O Medium 数组 | 数据搜索  
289 生命游戏 O Medium 数组 | 其它  
297 二叉树的序列化和反序列化 O Hard  
300 最长升序序列 O *Medium 数组 | 动态规划  
315 返回右边小于当前数的计数数组 O *Hard 数组 | 数据搜索 | 二叉搜索树  
324 摆动排序 O Medium 数组 | 排序 | 其它  
326 判断是否为3的幂 O Medium 数学  
328 奇偶链表 O Medium 链表  
329 二维数组最长递增路径 O Medium 数组 | 递归 |DFS  
334 递增的三元子序列 O Medium 动态规划  
347 前k个高频元素 O Medium 数据结构 | 红黑树  
350 求两个数组的交集 O Easy 数据结构 | 红黑树  
371 不使用+求两个数的和 O Easy 红黑树  
378 在一个排好序的二维数组中找到第k小的元素 O Medium 数据结构| 堆| 优先级队列  
380 常数时间完成数据插入,删除和随机返回 O Medium 数据结构 | 哈希表  
384 打乱数组 O Medium 数据结构 | 哈希表  
387 在字符串中找到第一个只出现一次的字符 O Easy 数据搜索 | 哈希表  
395 至少有K个重复字符的最长子串 O Medium 递归  
454 四数求和 O Medium 数据结构 | 哈希表  
538 Morris遍历实现中序遍历 O Medium  
# KMP求字符串匹配 O Hard 字符串  
# 判断字符串是否互为旋转词 O Medium 字符串  
# 换钱问题 O Medium 动态规划  
# 最长公共子序列 O Medium 动态规划  
# 最长公共子串 O Medium 动态规划  
# 最小编辑代价 O Medium 动态规划  
# 表达式得到期望结果的组成总数 O Hard 动态规划  
# 纸牌博弈问题 O Hard 动态规划  
# 添加最少字符使字符串整体都是回文字符串 O Hard 字符串|动态规划  
# 公式字符串求值 O Medium 字符串 | 递归  
# 0左边必有1的二进制字符串的数量 O Medium 字符串  
# 找到字符串的最长无重复字符子串 O Medium 字符串  
l660 一次读取4字节的接口实现读取N字节的接口 O Hard 设计  
# 找到无序数组中最小的k个数 O Medium 数组 / 数据搜索
# 需要排序的最短子数组长度 O Medium 数组  
# 在数组中找到出现次数大于N/K的数 O Hard 数组  
# 最长的可整合子数组的长度 O Hard 数组  
# 未排序正数数组中累加和为给定值的最长子数组长度 O Hard 数组  
# 未排序数组中累加和为给定值的最长子数组系列问题 O Hard 数组  
# 未排序数组中累加和小于或等于给定值的最长子数组 O Hard 数组  
# 计算数组的小和 O Hard 数组  
# 自然数数组的排序 O Easy 数组  
# 子矩阵的最大累加和问题 O Medium 数组  
# 在数组中找到一个局部最小的位置 O Easy 数组  
# 数组中子数组的最大累乘积 O Medium 数组  
# 求最短通路值 O Medium 数组  
# 从5随机到7随机及其扩展 O Medium 其它  
# 正数数组的最小不可组成和 O Medium 动态规划  
# 分糖果问题 O Medium 其它  
# 设计一个没有扩容负担的堆结构 O Hard 数据结构  
# 在两个长度相等的排序数组中找到上中位数 O Medium 数组  
# 在两个排序数组中找到第K小的数 O Medium 数组  
# 在两个有序数组间相加和的TopK问题 O Medium 数组  

数据搜索及查找

(哈希表,二分法)

LeetCode4

如何找到两个排序数组的中位数?(LeetCode4:Median of Two Sorted Arrays)

  • 描述:给定两个排序的数组a,b长度分别为m,n找出这两个数组的中位数,时间复杂度为O(log(m+n))

假定a,b长度分别大于k,现在选择第k个数(排序后第k个数),先将a的第k/2个元素(a[k/2-1])和b的第k/2个数进行比较,存在以下三种情况:

  1. a[k/2-1] == b[k/2-1],第k个数找到了,就是a[k/2-1]或b[k/2-1]
  2. a[k/2-1] > b[k/2-1],第k大的数肯定不可能在b[0..k/2-1]之间,因为小于b[k/2-1]的数包括b[0..k/2-1](k/2个)以及a[0..k/2-1](小于k/2个)中的一部分
  3. 同上,a[k/2-1] < b[k/2-1],第k大的数肯定不可能在a[0..k/2-1]之间

要是m,n其中有一个长度小于k/2,则取长度和k/2的较小值来进行切割。

LeetCode33

在翻转数组中查找指定数据(LeetCode33: Search in Rotated Sorted Array)

翻转数组在剑指offer中有相关描述,这题也是剑指offer面试题11的很好的延伸,思路利用二分法来进行数据搜索,需要注意的是,如何根据nums[mid]和查找目标target的大小关系进行范围的缩小,分为两种情况:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        if(nums.empty()){
            return -1;
        }
        int low = 0,high = nums.size()-1,mid = 0;
        while(low < high){
            mid = (low+high)/2;
            //情况1:中切节点后面的部分不为递增数列
            if(nums[mid]>nums[high]){
                if(nums[mid] > target){
                    if(target > nums[high]){
                        high = mid -1;
                    }else if((target < nums[high])){
                        low = mid + 1;
                    }else {
                        return high;
                    }     
                }else if(nums[mid] < target){
                    low = mid + 1;
                }else{
                    return mid;
                }
            }else if(nums[mid]<=nums[high] ){
            // 情况2:中切节点后面的部分为递增序列
                if(nums[mid] > target){
                    high = mid -1;
                }else if(nums[mid] < target){
                    if(target > nums[high]){
                        high = mid - 1;
                    }else if(target < nums[high]){
                        low = mid +1;
                    }else{
                        return high;
                    }

                }else{
                    return mid;
                }
            }
        }
        //确定最后一个元素是否等于target
        if(nums[low] == target)
            return low;
        
        return -1;
        
    }
};

LeetCode34

搜索一个数字在一个递增数组(允许数字重复)中出现的范围(LeetCode34:Search for a Range)

返回出现的下标范围[start ,end],如果不存在返回[-1,-1]

思路:二分法

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int low = 0,high = nums.size()-1;
        int mid;
        int start = -1, end = -1;
        vector<int> result;
        if(nums.empty()){
            result.push_back(start);
            result.push_back(end);
            return result;
        }
        //开始二分查找,只要找到就返回
        while(low<high){
            mid = (low+high)/2;
            if(nums[mid] > target){
                high = mid -1;
            }else if(nums[mid] < target){
                low = mid + 1;
            }else{
                start = end = mid;
                break;
            }
        }
        //查看最后的位置是否即为目标数
        if(nums[low] == target){
            start = end = low;
        }
        //确定前后范围
        if(start >= 0){
            while(start>0){
                if(nums[start-1] == target)
                    start--;
                else
                    break;
            }
            
            while(end < nums.size()-1){
                if(nums[end+1] == target)
                    end++;
                else 
                    break;
            }
        }

        result.push_back(start);
        result.push_back(end);
        return result;
    }
};

LeetCode36

判断数独二维数组是否合法(LeetCode36: Valid Sudoku)

  • 为什么要把这一题放到数据搜索及查找这一分类?因为判断一个数独数组是否合法可以分解为在纵向列,横向行,及每个子box中寻找重复的数字(1-9),只要有重复的数字就不合法,没有就合法。
  • 而寻找重复的数字在剑指offer中有对应的题目:面试题3,哈希表的思路

我先介绍自己的思路:利用三个map保存每一行,每一列,每个子box是否有效,然后逐个字符来进行判断。

class Solution {
public:
    bool isValidSudoku(vector<vector<char>>& board) {
        map<int,bool> row_map,column_map,box_map;//三个记录每行,每列,每个子 box是否合法的map
        set<char> test_set;
        
        //初始化
        for(int i = 0;i<9;i++){
            row_map.insert(make_pair(i,false));
            column_map.insert(make_pair(i,false));
            box_map.insert(make_pair(i,false));
        }
        //循环遍历二维数组中的每个元素
        for(int i = 0;i<9;i++){
            for (int j = 0;j<9;j++){
                
                //对于每一个元素,先测试所在行是否合法
                if (board[i][j]!='.'){
                    if(!column_map[i]){
                        //还没被测试过,开始测试,如果已经测试为真,则直接跳过
                        for(int k = j;k<9;k++){
                            if(board[i][k]!='.'){
                                if(test_set.count(board[i][k])){
                                    return false;
                                }else{
                                    test_set.insert(board[i][k]);
                                }
                            }
                        }
                        column_map[i] = true;
                    }
                    
                    test_set.clear();//清楚测试集,用于下一个测试

                    //开始测试所在列是否合法,若之前已经被测试过,则直接跳过
                    if(!row_map[j]){
                        //还没测试过,开始检查
                        for(int k = i;k<9;k++){
                            if(board[k][j]!='.'){
                                if(test_set.count(board[k][j])){
                                    return false;
                                }else{
                                    test_set.insert(board[k][j]);
                                }
                            }
                        }
                        row_map[j] = true;
                    }
                    
                    test_set.clear();
                    int index = (i/3)*3 + j/3;
                    //开始测试所在子box是否合法
                    if(!box_map[index]){
                        //开始检查
                        int start_col = (index/3)*3;
                        int start_row = (index%3)*3;
                        for(int col = start_col;col<start_col+3;col++){
                            for(int row = start_row;row<start_row+3;row++){
                                if(board[col][row]!='.'){
                                    if(test_set.count(board[col][row])){
                                        return false;
                                    }else{
                                        test_set.insert(board[col][row]);
                                    }
                                }
                            }
                        }
                        box_map[index] = true;
                    }
                    test_set.clear();
                }
                
            }
        }
        //当所有元素遍历检查结束后没出现不合法情况,则合法
        return true;
        
    }
};

哈希表的思路:分别创建用于测试行,列,子box的三个二维哈希表。

class Solution
{
public:
    bool isValidSudoku(vector<vector<char> > &board)
    {
        int used1[9][9] = {0}, used2[9][9] = {0}, used3[9][9] = {0};//三个用于测试的哈希表,初始化为0
        
        for(int i = 0; i < board.size(); ++ i)
            for(int j = 0; j < board[i].size(); ++ j)
                if(board[i][j] != '.')
                {
                    int num = board[i][j] - '0' - 1, k = i / 3 * 3 + j / 3;
                    if(used1[i][num] || used2[j][num] || used3[k][num])
                    //所在位置出现重复,说明所在行,所在列或所在子box有重复的元素,不合法
                        return false;
                    used1[i][num] = used2[j][num] = used3[k][num] = 1;//之前没出现过则将该数值存放到行,列,子box三个哈希表中和数值相等的下标的位置上去。
                }
        
        return true;
    }
};

LeetCode41

找到第一个未出现的正数(LeetCode41: First Missing Positive)

  • 描述:给出一个未排序的整型数组(元素可以重复),找出所有正数1,2,3…开始第一个没出现在该数组中的元素

如:

  • [ 1,2,0 ]:输出3
  • [ -1,3,4,1 ]:输出2

思路: 哈希表,将自身当作一个hash表,遍历这个数组,将每一个数放置到该数对应的下标上去 如果:

- 出现该数对应的下标超出该数组的范围或者该数为非正数,则和尾部**第一个没有超出**数组大小的数进行互换,互换后尾部的数据则**不用考虑**,且**需要检测**的数组的大小也对应缩小
- 该数对应的下标的位置上已经有正确的数(**重复**),同样和数组尾部第一个没有超出数组大小的数进行互换,互换后尾部的数据则不用考虑,且需要检测的数组的大小也对应缩小

所有的数正确安排结束后,则开始从头开始遍历,第一个和下标不能一一对应的数就是第一个没有出现的正数,返回

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        if(nums.empty()){
            return 1;
        }
        int size = nums.size();
        int tmp;
        int end = size;
        for(int i = 0;i<end;i++){
            if(nums[i] != i+1){
                if(nums[i] <= 0||nums[i] > end|| nums[nums[i]-1] == nums[i]){//需要放置到尾部不予考虑
                    while(end-1 > i && nums[end-1] >= end){
                        end--;//在尾部找到第一个合法的数
                    }
                    if(end-1>i){
                        tmp = nums[end-1];
                        nums[end-1] = nums[i];
                        nums[i] = tmp;
                        i--;
                        end--;
                    }
                }else {
                //放置到正确的下标位置
                    tmp = nums[nums[i]-1];
                    nums[nums[i]-1] = nums[i];
                    nums[i] = tmp;
                    i--;
                }
            }
        }
        
        int k = 0;
        while(k<size && nums[k] == k+1){
            k++;//遍历得到第一个没有出现的正数
        }
        return k+1;
    }
};

LeetCode73

设置矩阵0项的行和列(LeetCode73:Set Matrix Zeroes)

给出一个二维矩阵,对于该矩阵中的每一个为0的元素,将其所在的行和列全部设为0。

Input: 
[
  [1,1,1],
  [1,0,1],
  [1,1,1]
]
Output: 
[
  [1,0,1],
  [0,0,0],
  [1,0,1]
]

Input: 
[
  [0,1,2,0],
  [3,4,5,2],
  [1,3,1,5]
]
Output: 
[
  [0,0,0,0],
  [0,4,5,0],
  [0,3,1,0]
]

思路:哈希表。一个矩阵一共有m行,n列,每一行和每一列我们都需要了解其是否应该被设置为0的信息。将这个信息和它们所在的行列下标联系起来:

  • 所以我们可以用第一行的元素 matrix[0][j]来记录所有列是否该设为0,若需要,则将第一行对应列的元素也设为0。
  • 对于行是否该设为0 的信息则保存在第一列
  • 但由于第一行和第一列的信息发生重合,所以我们将第一行的信息,保存在额外的变量col0

代码实现:

void setZeroes(vector<vector<int> > &matrix) {
    int col0 = 1, rows = matrix.size(), cols = matrix[0].size();

    for (int i = 0; i < rows; i++) {//将每一行和每一列是否该设置为0的信息保存在对应的位置
        if (matrix[i][0] == 0) col0 = 0;
        for (int j = 1; j < cols; j++)
            if (matrix[i][j] == 0)
                matrix[i][0] = matrix[0][j] = 0;
    }

    for (int i = rows - 1; i >= 0; i--) {//根据行和列保存的信息设置每一个元素
        for (int j = cols - 1; j >= 1; j--)
            if (matrix[i][0] == 0 || matrix[0][j] == 0)
                matrix[i][j] = 0;
        if (col0 == 0) matrix[i][0] = 0;
    }
}

LeetCode128

最长连续序列(LeetCode128:Longest Consecutive Sequence)

给一个整型数组,找出该数组中的整型数最长的连续序列的长度。

Input: [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.

要求:O(N)的复杂度

思路:使用哈希表,结合C++中的关联容器unordered_set来使用哈希表结构


class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        if(nums.empty())
            return 0;
        unordered_multiset<int> num_set(nums.begin(),nums.end());//将数组中的元素添加到以hashtable为基础的unordered_set数据结构中
        
        int curnum,len=1,maxlen=1;
        for(auto num : num_set){
            if(num_set.find(num-1) == num_set.end()){//数组中没有当前数字的前一个数字存在,如果有,前面已经被计算过,不用重复计算
                curnum = num;
                len = 1;
                while(num_set.find(curnum+1) != num_set.end()){//获取以当前整形数为起点的最长连续序列的长度
                    curnum++;
                    len++;
                }
                maxlen = max(maxlen,len);
            }
        }
        
        return maxlen;
    }
};

LeetCode146

LRU缓存(LeetCode146:LRU Cache)

题目:实现一个LRU缓存类,缓存替换策略使用LRU最近最少使用策略,实现获取数据的接口get(key),实现添加数据的接口put(key,value);

这两种操作的复杂度限制在O(1);

LRUCache cache = new LRUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.put(4, 4);    // evicts key 1
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

思路:

  1. 获取数据(数据查找)的复杂度为O(1)要求必须使用哈希表作为索引实现
  2. 插入数据的复杂度为O(1)要求使用链表来实现复杂度
  3. 所以我们结合使用链表和哈希表的结构,数据缓存使用链表结构,然后所有链表中的节点和哈希表中的元素相关联,从而达到O(1)快速索引的目的。

leetcode146

代码实现:

class LRUCache {
public:
    LRUCache(int capacity) {
        this->capacity_ = capacity;
    }
    
    int get(int key) {
        if(key_map.find(key)!=key_map.end()){//key搜索由哈希表结构unordered_map完成
            cache_.splice(cache_.begin(),cache_,key_map[key]);//链表操作,将某连续范围内的元素从一个list移动到另一个list的某个定点
            return key_map[key]->second;
        }
        
           return -1;
    }
    
    void put(int key, int value) {
        if(key_map.find(key)!=key_map.end()){
            key_map[key]->second = value;
            cache_.splice(cache_.begin(),cache_,key_map[key]);
            return;
        }
        
        if(cache_.size() == capacity_){//当前缓冲区满时,删除链表尾端
            int key = cache_.back().first;
            key_map.erase(key);
            cache_.pop_back();
        }
        
        cache_.emplace_front(key,value);
        auto iter = cache_.begin();
        key_map[key] = iter;
    }
private:
    int capacity_;
    list<pair<int,int>> cache_;//缓存存放数据
    unordered_map <int,list<pair<int,int>>::iterator> key_map;//哈希表构建缓存链表的每个节点的快速索引
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

LeetCode215

查找第n大的元素(LeetCode215:Kth Largest Element in an Array)

给出一个数组,查找出其中第n大的元素

Input: [3,2,1,5,6,4] and k = 2
Output: 5

Input: [3,2,3,1,2,4,5,5,6] and k = 4
Output: 4

思路:

  1. 快速排序
  2. 堆排序

堆排序的实现:

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        make_heap(nums.begin(), nums.end());//构建堆
        auto last = nums.end();
        auto start = nums.begin();
        for (int i = 0; i<k - 1; i++)
            pop_heap(start, last--);//因为堆排序每次会将pop的元素放置到数组末端,所以last--不断减少其规模
        return nums.front();
    }
};

快速排序的实现:

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        if(nums.size()==0 || k > nums.size())
            return -1;
        int start = 0;
        int end = nums.size()-1;
        while(start<=end)
        {
            int index = Partition(nums,start,end);//进行快排
            if(index == k-1) return nums[index];//直到找到第k大的元素
            else if(index > k-1) end = index - 1;
            else start = index + 1;//不断调整进行快排的范围
        }
        return -1;
    }
private:
    int Partition(vector<int>& nums, int start, int end)
    {
        int pivot = nums[end];//将最后一个元素作为枢纽元
        int big = start-1;
        for(int i=start; i<end; ++i)
        {
            if(nums[i]>pivot)//将大于该枢纽的元素放到右边
            {
                ++big;
                swap(nums[big],nums[i]);
            }
        }
        ++big;
        swap(nums[end],nums[big]);//将枢纽元放到合适的位置
        return big;
    }
};

LeetCode217

查找是否存在重复元素(LeetCode217:Cantains Duplicate)

给出一个数组,查找其中是否存在重复元素

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

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

思路:

  1. 哈希表
  2. 利用map不可插入重复元素的性质
class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        set<int> num_set;
        pair<set<int>::iterator,bool> res;
        for(auto num:nums){
            res = num_set.insert(num);//插入操作返回一个pair,第二元素包含是否插入成功
            if(!res.second)
                return true;
        }
        return false;
    }
};

LeetCode240

二位数组搜索指定数值(LeetCode240:Search a 2D Matrix II)

给一个二维数组,每一行保持递增关系,每一列也保持递增关系,在这样的数组中搜索我们指定的数值。

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

Given target = 5, return true.

Given target = 20, return false.

思路:见剑指offer:面试题四:二维数组中的查找(对顺序容器的访问)

代码实现:

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if(matrix.empty())
            return false;
        int i =0,j=matrix[0].size()-1;
        while(i<matrix.size()&&j>=0){
            if(matrix[i][j]>target)
                j--;
            else if(matrix[i][j]<target)
                i++;
            else
                return true;
        }
        return false;
    }
};

LeetCode287

找到重复数字(LeetCode287:Find the Duplicate Number)

给出一个数组,该数组包含n+1个元素,所有数据的范围在1~n之间,其中只有一个数字重复出现,且重复的次数并不清楚,找到该重复的数字。

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

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

要求:

  1. 不能修改数组
  2. 只能使用O(1)的内存空间
  3. 时间复杂度小于O(n*n)

若能够修改数组,我们可以使用哈希表的思路来直接求解

代码实现:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int result = nums[0];
        for (int i = 0;i<nums.size();i++){
            if(i!=nums[i]-1){
                if( nums[nums[i]-1]!=nums[i]){
                    swap(nums[i],nums[nums[i]-1]);
                    i--;
                }
                else return nums[i];
            }
                
        }
        return 0;
    }
};

不能修改数组,则只能使用二分查找,将1~n(如9)分成两部分

  1. 1~9分为1~5和6~9两部分
  2. 遍历数组,计数有多少个在1~5之间的数,若计数的个数大于5(5-1+1),则说明1~5之间有重复的数字,否则6~9之间有重复的数字
  3. 循环1,2步骤,继续缩小范围,直到找到重复的数字

代码实现:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int low = 1,high = nums.size()-1,mid;
        int count = 0;
        while(low<high){
            count = 0;
            mid = (low+high)/2;
            for(int i = 0;i<nums.size();i++){
                if(nums[i]<=mid&&nums[i]>=low)
                    count++;
            }
            if(count>mid-low+1){
                high = mid;
            }else{
                low = mid+1;
            }
        }
        return low;
    }
};

LeetCode315

返回右端小于当前元素的计数数组(LeetCode315:Count of Smaller Numbers After Self)

给出一个数组,返回一个数组,返回的数组中的元素代表,原数组中当前下标的元素x的右端有多少个小于x的元素

Input: [5,2,6,1]
Output: [2,1,1,0] 
Explanation:
To the right of 5 there are 2 smaller elements (2 and 1).
To the right of 2 there is only 1 smaller element (1).
To the right of 6 there is 1 smaller element (1).
To the right of 1 there is 0 smaller element.

思路1:暴力求解 O(N^2)

class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        int count = 0;
        vector<int> result(nums.size());
        for(int i=nums.size()-1;i>=0;i--){
            count = 0;
            for(int j=nums.size()-1;j>i;j--){
                if(nums[j]<nums[i])
                    count++;
            }
            result[i] = count;
        }
        return result;
    }
};

思路2:

参考思路

二叉树中的每个节点记录三个信息:当前节点值,当前节点值得重复个数,当前节点的左子树节点个数

leetcode315

  1. 使用二叉搜索树,提高效率
  2. 构建二叉树,从数组的右端开始
  3. 当遍历到x元素时,先在二叉树中找到元素x合适的插入节点,在递归遍历过程的路径中,当当前节点小于x时:
    • 若当前节点没有右子树,则插入到当前节点的右孩子节点,并返回当前节点的左子树(小于该节点元素的个数)的节点个数和当前节点的重复个数。
    • 若当前节点有右子树,返回当前节点的重复计数+左子树节点个数+右子树根节点递归遍历返回的数量
  4. 递归遍历过程的路径中,当当前节点大于x时:
    • 按照步骤3递归左子树根节点,直接返回递归的结果值即可

代码实现:

struct BSTNode{
    BSTNode(int v):val(v),count(1),left_count(0),left(nullptr),right(nullptr){}
    ~BSTNode(){
        if(left) delete left;
        if(right) delete right;
    }
    //返回小于等于该节点的值
    int less_or_equal()
    {
        return count+left_count;
    }
    int val;
    int count;
    int left_count;
    BSTNode* left;
    BSTNode* right;
};
class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        if(nums.empty()) return {};
        vector<int> res;
        //初始化nums最右数的值
        res.push_back(0);
        BSTNode* root = new BSTNode(nums[nums.size()-1]);
        for(int i=nums.size()-2; i>=0; --i)
            res.push_back(insert(root,nums[i]));//从最右边的元素开始遍历
        reverse(res.begin(),res.end());
        return res;
    }
    int insert(BSTNode* root, int num)
    {
        if(num == root->val)
        {
            root->count++;
            return root->left_count;
        }
        else if(num < root->val)//小于当前元素时。往左边插入时
        {
            ++(root->left_count);
            if(root->left==nullptr)
            {
                root->left = new BSTNode(num);   
                return 0;
            }
            return insert(root->left,num);//直接返回递归结果
        }
        else//大于当前元素,往右边插入时
        {
            if(root->right==nullptr)//右子树为空时
            {
                root->right = new BSTNode(num);
                return root->less_or_equal();
            }
            return root->less_or_equal() + insert(root->right,num);//右子树不为空时
        }
    }
};

LeetCode162

找出山顶元素(LeetCode162:Find Peak Element)

题目:给出一个数组,找出”山顶”元素,山顶元素就是该元素比它两边的元素都大的元素

假设nums[-1] = nums[n] = -∞

可能有多个山顶元素,返回其中的一个即可

Input: nums = [1,2,3,1]
Output: 2
Explanation: 3 is a peak element and your function should return the index number 2.

Input: nums = [1,2,1,3,5,6,4]
Output: 1 or 5 
Explanation: Your function can return either index number 1 where the peak element is 2, 
             or index number 5 where the peak element is 6.

思路1:遍历所有节点,找到第一个出现”下降”位置的元素。复杂度O(N)

思路2:二分法。

  • 找到中间元素mid
  • 若nums[mid] > nums[mid+1],则在前半部分肯定有山顶元素的存在,因为nums[0] = -∞
  • 若nums[mid] < nums[mid+1],则在后半部分肯定有山顶元素的存在,因为nums[n] = -∞

代码:

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int low=0,high=nums.size()-1;
        int mid;
        if(high == 0)
            return 0;
        while(low<high){
            mid = (low+high)/2;
            if(nums[mid] > nums[mid+1]) 
                high = mid;
            else if(nums[mid] < nums[mid+1])
                low = mid+1;
        }
        
        return low;
            
    }
};

LeetCode387

字符串中第一个只出现一次的字符(LeetCode387:First Unique Character in a String)

给出一个字符串,找到该字符串中第一个只出现一次的字符的下标

s = "leetcode"
return 0.

s = "loveleetcode",
return 2.

思路:使用哈希表进行记录

class Solution {
public:
    int firstUniqChar(string s) {
        unordered_map<char,int> record;
        for(int i = 0;i< s.size();i++){
                record[s[i]]++;//使用hash表记录
        }
        
        for(int i = 0;i<s.size();i++){
            if(record[s[i]] == 1)
                return i;
        }
        return -1;
            
    }
};

code336

找到无序数组中最小的k个数

给定一个整型数组arr,找到其中最小的k个元素

要求:

  • 时间复杂度为O(NlogN)
  • 进阶要求:时间复杂度为O(N)

思路1:

  • 使用堆结构来维护最小k个数,复杂度为O(NlogN)

思路2:BFPRT算法 select(int[] arr,k),复杂度分析:O(N)

  1. 将arr中的n个元素划分为n/5,每组5个元素,组内进行插入排序,最后一组不够5个元素为一组
  2. 将每组的中位数提取出来构成新的数组midarr[]
  3. 递归调用select(midarr,midarr.len/2)找到中位数x
  4. 根据x进行partition,比x小的在x左边,比x大的都在x右边,x所在的位置为i
    • 如果x的位置i == k则x为第k小的数,返回
    • 如果i < k,则第k小的数在x的右边,递归调用select,查找第k-i小的数
    • 如果i > k,则第k小的数在x的左边,递归调用select,查找第k小的数

数据搜索和查找ending

动态规划

LeetCode5

找到最长回文子字符串(LeetCode5:Longest Palindromic Substring)

  • 描述:给出一个字符串,找出它的最长且为回文结构的子字符串:如:“babad”的最长子字符串且为回文的子串为”bab”,或“aba”,而”cbbd”的最长子串为“bb”

思路1:暴力求解:找出所有可能的子字符串头i和尾j,然后判断是否为回文,复杂度O(N^3)

思路2: 动态规划,用一个二维数组arr记录子字符串是否为回文,横坐标纪录字符串开头i(i < s.zise()),纵坐标纪录结尾j(i<=j< s.size());然后二维数组中纪录的是从i到j的子字符串是否为一个回文结构。回文结构由以下三种情况

* 由**一个字符**组成的回文字符串
* 由**两个相同的字符**组成回文字符串
* i到j字符串为回文字符串,则i+1到j也为回文字符串,且s[i] = s[j];

所以根据上述关系arr[i][j]和arr[i+1][j-1]的关系来做状态转换方程构建该二维数组。构建过程的复杂度为O(N^2),然后根据该二维数组找出最长子字符串。空间复杂度也为O(N^2)

思路3(中心扩展,时间复杂度O(N^2),空间复杂度O(1)): 回文结构其实就是以一个点为中心,向两边扩展,我们只要找到这2n-1个中心(n为字符串的长度)然后两边扩展即可得到回文长度即可。为什么是2n-1个中心,因为中心分为两种:

* 以一个字符为中心
* 以两个相同的字符为中心
public String longestPalindrome(String s) {
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;
}

思路4(Manacher’s Algorithm,O(N)):

首先将每两个字符串中插入”#”,确保每个回文字符串都是奇数形式,以一个字符为中心的回文字符串

#1#2#2#1#2#2#

为了防止越界,我们还需要在首尾再加上非#号字符

遍历字符串,当遍历到第i字符时,我们需要在遍历过程中纪录回文串可以延伸到最右端的端点下标max_right,和以该端点作为结尾的回文字符串的中心点ct。还要纪录以该点为中心点的回文串的长度len[i]

  • 假设当我们遍历到ct时,匹配后确定了以它为中心的回文串达到的右端点为前面所有中心的回文串的最右端点:max_right。
  • 遍历下一个节点j = ct+1时,len[j] = max_right > j ? min(p[2*ct-j],max_right - j):1;
    • 这个表达式的含义就是当这个下标还在我们前面遍历得到回文串的最右端点的范围之内时,根据回文串的映射关系:2ct-j为j以ct为中心的映射点,在ct为中心的回文串范围中,j和2ct-j的回文串长度应该保持一致,但是出了这个范围,就需要进一步匹配来确定: LeetCode_1.png
    • 遍历过程中需要纪录:回文串最右端点及中心点,最长回文字符串的中心点及长度。遍历一次更新一次
    • 代码参考:https://www.cnblogs.com/grandyang/p/4475985.html

LeetCode10

字符串匹配“.*”(LeetCode10:Regular Expression Matching)

题目链接: https://leetcode.com/problems/regular-expression-matching/description/ 描述:给定一个字符串匹配模式pattern,输入一个字符串判断是否与该pattern匹配。

  1. ’.’代表任意字符
  2. ‘a*‘代表a可以出现0次,1次或任意次
  3. ’.*‘带表任意长度(包括0)任意类型的字符串

思路1:递归

处理这三种匹配情况

  1. 碰到字母或’.’时,只要相等就进行下个字符的匹配
  2. 碰到*时分两种情况

    • 字符+‘*’匹配0个字符,1个字符或多个重复字符
    • ’.*‘匹配0个字符,1个任意字符或多个任意长度的任意类型的字符
bool match(string & str,string & pattern,int str_i,int pat_i){
        if(str_i == str.size() && pat_i == pattern.size()){
                return true;
        }
        if(str_i>str.size())
            return false;
        bool result = false;
        if(pattern[pat_i+1]!='*'){
        /*处理非*的情况*/
            if(str[str_i] == pattern[pat_i]||pattern[pat_i] == '.')
                return match(str, pattern, str_i+1, pat_i+1);
            return result;
        }else {
            if(pattern[pat_i] != '.'){
            /*处理字符+‘*’的情况*/
                result = match(str, pattern, str_i, pat_i+2);//字符+‘*’匹配0个字符串
                if(result)
                    return result;
                if(str[str_i] == pattern[pat_i])
                    result = match(str, pattern, str_i+1, pat_i+2)||match(str, pattern, str_i+1, pat_i);//字符+‘*’匹配1个字符串或多个重复字符
                return result;
            }else{
            /*处理‘.*’的情况*/
                result = match(str, pattern, str_i, pat_i+2)||match(str, pattern, str_i+1, pat_i+2)||match(str, pattern, str_i+1, pat_i);;//‘.*’匹配0个字符,1个字符,任意长度的任意类型的字符
                return result;
            }
        }
    };
}

思路2: 动态规划

用一个二维数组memo[pat_i+1][str_j+1]来记录长度为str_j和长度为pat_i的模版是否匹配

  1. 先进行初始化
  2. 状态转换方程

    • 碰到pattern[pat_i]为字母或‘.’时
    • 碰到pattern[pat_i]为为‘*’时
      • pattern[pat_i-1]为普通字符时
      • pattern[pat_i-1]为‘.’时
  3. 返回memo[pat_i+1][str_j+1]
bool match_dp(string & str,string & pattern){
        int str_size = (int)str.size(),pat_size = (int)pattern.size();
        bool ** memo =new bool*[pat_size+1];//二维数组的创建
        for(int i =0;i<=pat_size;i++){
            memo[i] = new bool[str_size+1];
        }
        for(int i = 0;i<=pat_size;i++)
            for(int j = 0;j<=str_size;j++)
                memo[i][j] = false;
        //Initialize初始化
        memo[0][0] = true;
        if(str_size>0&&pat_size>0){
            if(str[0] == pattern[0]||pattern[0] == '.')
                memo[1][1] = true;
        }
        if(pat_size<2){
            return memo[pat_size][str_size];
        }
        //Dynamic Programming,开始进行遍历
        bool result = false;
        for(int i = 2;i<=pat_size;i++)
        {
            for(int j = 0;j<=str_size;j++){
                result = false;
                if(pattern[i-1] == '*'){
                    if(pattern[i-2]!='.'){
                        result =result||memo[i-2][j];//字母+*匹配0个字符
                        if(str[j-1] == pattern[i-2])
                            result = result || memo[i-2][j-1]||memo[i][j-1];//字母+*匹配1个字符或多个字符
                    }else{
                    /* 处理情况‘.*’  */
                        result = memo[i][j-1]||memo[i-2][j];//.*匹配一个或多个字符,.*匹配0个字符
                    }
                }else{
                /* 处理情况‘非*’*/
                    if(str[j-1] == pattern[i-1]||pattern[i-1] == '.'){
                        result = memo[i-1][j-1];
                    }
                }
                memo[i][j] = result;
            }
        }
        return memo[pat_size][str_size];
    }

LeetCode44

字符串匹配:”?*“(LeetCode44:Wildcard Matching)

描述:给定一个字符串匹配模式pattern,输入一个字符串判断是否与该pattern匹配。

’?’可以代替任意一个字符,’*‘可以代替任意长度任意类型的字符串

方法有两种:

  • 回溯法
  • 动态规划二维数组
    • 优化版本:只用一维数组记录数据
class Solution {
    
    public:
    //回溯法,比较耗时,递归调用
    bool isMatch(string s, string p) {
        int s_end = s.size();
        int p_end = p.size();
        return ismatch(s,p,0,0,s_end,p_end);
    }
    
    bool ismatch(string &s,string &p,int i,int j, int s_end,int p_end){
        bool result = false;
        if(i == s_end){
            if(j == p_end)
                return true;
            else if(p[j] == '*')
                return ismatch(s,p,i,j+1,s_end,p_end);
            else
                return false;
        }
        
        if(i>s_end || j>p_end)
            return false;
        if(j < p_end){
            if(p[j] == '?')
                result = ismatch(s,p,i+1,j+1,s_end,p_end);//匹配一个,均前进一步
            else if(p[j] == '*')
                result = ismatch(s,p,i,j+1,s_end,p_end)||ismatch(s,p,i+1,j+1,s_end,p_end)||ismatch(s,p,i+1,j,s_end,p_end);//'*'分别匹配0个,1个和多个
            else if(s[i] == p[j])
                result = ismatch(s,p,i+1,j+1,s_end,p_end);//匹配一个,均前进一步
            else
                result = false;
        }
        
        return result;
        
        
        
    }
    //二维数组进行动态规划
        bool isMatch_dp2(string s, string p) {
            const int s_end = s.size(),p_end = p.size();
            int** record = new int*[p_end+1];//二维数组纪录结果数据
            
            //初始化基础数据
            for(int i=0;i<=p_end;i++){
                record[i] = new int[s_end+1];
            }
            
            for(int i=0;i<=p_end;i++){
                for(int j = 0;j<=s_end;j++){
                    record[i][j] = 0;
                }
            }
            
            record[0][0] = true;
            if(p_end>0){
                //初始化基础数据
                if(p[0] == '*'){
                    for(int i = 0;i<=s_end;i++)
                        record[1][i] = 1;
                }else if(p[0] == '?'||p[0] == s[0]){
                    record[1][1] = 1;
                }
                
                for(int i = 0;i<p_end;i++)
                    if(p[i] == '*')
                        record[i+1][0] = record[i][0];
                
                //开始动态规划
                for(int i=2;i<=p_end;i++){
                    for(int j = 0;j<=s_end;j++){
                        if(p[i-1] == '*'){
                            for(int k = 0;k<=j;k++){
                                if(record[i-1][k])
                                    record[i][j] = 1;
                            }
                        }
                        else if(p[i-1] == '?'){
                            record[i][j] = record[i-1][j-1];
                        }else if(p[i-1] == s[j-1]){
                            record[i][j] = record[i-1][j-1];
                        }
                    }
                }
            }
            
            if(record[p_end][s_end]){
                return true;
            }
            else
                return false;
        }
    //一维数组进行动态规划
    bool isMatch_dp1(string s, string p) {
        int s_end = s.size(),p_end = p.size();
        int *record = new int[s_end+1];//用一维数组纪录结果
        record[0] = 1;
        if(s_end >0){//初始化基础数据
            for(int i = 1;i<s_end+1;i++){
                record[i] = 0;
            }
        }
        
        if(p_end>0){
            for(int i = 1;i<=p_end;i++){
                for(int j = s_end;j>=0;j--){//倒序进行确认
                    if(p[i-1] == '*'){
                        for(int k = 0;k<=j;k++){
                            if(record[k] == 1){
                                record[j] = 1;
                                break;
                            }
                        }
                    }
                    else if(p[i-1] == '?'){
                        record[j] = record[j-1];
                    }else if(p[i-1] == s[j-1]){
                        record[j] = record[j-1];
                    }else{
                        record[j] = 0;
                    }
                }
            }
        }
        
        return record[s_end];
    }


};

LeetCode38

计数后用字符串表达(LeetCode38:Count and Say)

有一个字符串的序列,下一项为上一项的计数表达,从”1”开始计数

1. "1"
2. "11"  //1个1
3. "21"  //2个1
4. "1211"   //1个2,1个1
5. "111221"   //1个1,1个2,2个1
...

按照这个规律,求的第n项的字符串是什么

思路:动态规划,只需要一个字符串记录第n-1项字符串,然后开始计数后写入第n项字符串。

class Solution {
public:
    string countAndSay(int n) {
        if(n<=0)
            return NULL;
        string record = "1";//用于记录上一项的字符串
        string tmp;//用于迭代得到本轮字符串
        int count;
        char num;
        if(n==1){
            return record;
        }
        for(int k = 1;k < n;k++){
            for(int i=0;i < record.size();i++){
                num = record[i];
                count = 0;
                while(i < record.size()&&record[i] == num){
                    count++;
                    i++;
                }
                i--;
                tmp.push_back(count+'0');
                tmp.push_back(num);
            }
            record = tmp;
            tmp.clear();
        }
        return record;
    }
};

LeetCode53

最大子序列和(LeetCode53:Maximum Subarray)

给一个数组,求出该数组中和最大的子序列。

思路:循环遍历数组,记录每一个数结尾的最大子序列和,当该和小于0时重新从0开始求和。遍历结束后获取最大的子序列和。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.empty())
            return 0;
        int sum = 0,sum_max = nums[0];
        for(int i = 0;i<nums.size();i++){
            sum += nums[i];
            if(sum > sum_max)//寻找最大的子序列和
                sum_max = sum;
            if(sum < 0)//小于0时从0开始重新求和
                sum = 0;
        }
        
        return sum_max;
    }
};

LeetCode62

机器人走格子路径(LeetCode62:Unique Paths)

有一个mxn的棋盘,机器人要从起点(1,1)走到终点(m,n), 且机器人只能向右前进和向下前进。问机器人一共有多少种方式,或有多少条不同的路径可以到达终点?

robot_path

Input: m = 3, n = 2
Output: 3
Explanation:
From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:
1. Right -> Right -> Down
2. Right -> Down -> Right
3. Down -> Right -> Right

思路1:回溯法,递归耗时

class Solution {
public:
    int uniquePaths(int m, int n) {
        int result = 0;
        int *result_ptr = &result;
        uniquePath_x(1,1,m,n,result_ptr);
        return result;
    }
    
    void uniquePath_x(int x, int y, int m, int n,int * count){
        if(x == m&&y==n ){
            *count +=1;
            return;
        }
        if(x < m )
            uniquePath_x(x+1,y,m,n,count);
        if(y < n )
        return;
    }
};

思路2:动态规划,用一维数组即可


class Solution {
public:
    int uniquePaths(int m, int n) {
        if(m<=0||n<=0)
            return 0;
        if(m == 1|| n == 1)
            return 1;
        int *record = (int *)malloc(m*sizeof(int));
        
        for(int i = 0;i<m;i++){
            record[i] = 1;
        }
        
        for(int i =1;i<n;i++)
            for(int j = 1;j<m;j++){
                record[j] = record[j-1]+record[j];
            }
        
        return record[m-1];
    }
    

};

LeetCode91

求解码方法总数(LeetCode91:Decode Ways)

给出一串字符串,按如下规则进行解码,问有多少解码的方法?

'A' -> 1
'B' -> 2
...
'Z' -> 26

Input: "12"
Output: 2
Explanation: It could be decoded as "AB" (1 2) or "L" (12).

Input: "226"
Output: 3
Explanation: It could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

Input: "0"
Output: "0"

Input: "100"
Output: "0"

思路:动态规划,用一个数组记录从0开始记录字符串前n个字符构成的字符串有多少种方法解码,然后根据依赖关系求得前n+1个字符有多少种方法解码?

注意:因为0没有对应的解码,所以当碰到0时需要特殊注意,因为0只能和前一个元素进行结合,若结合失败,则返回0。


class Solution {
public:
    int numDecodings(string s) {
        if(s.empty())
            return 0;
        if(s.size() == 1){
            if(s[0] == '0')
                return 0;
            else
                return 1;
        }
        
        if(s[0] == '0')
                return 0;
        vector<int> record(s.size()+1,0);
        record[0] = 1;
        record[1] = 1;
        int num;
        for(int i = 1;i < s.size();i++){
            if(s[i-1] == '0'){//当前面的元素为0时的处理步骤
                if(s[i] == '0')//连续为0,返回0
                    return 0;
                record[i+1] = record[i];
            }
            else if(s[i]!='0'){//当前元素不为0时的处理步骤
                num = (s[i-1]-'0')*10 + s[i] - '0';//和前面的元素结合起来时的值
                if(num>0 && num <= 26)
                    record[i+1] = record[i-1] + record[i];//可以结合
                else
                    record[i+1] = record[i];//不可以结合
            }else if(s[i] == '0'){//当前元素为0时的处理步骤
                num = (s[i-1]-'0')*10 + s[i] - '0';//只能和前面的数结合
                if(num>0 && num <= 26)
                    record[i+1] = record[i-1];//结合的数合法
                else
                    return 0;//不合法时,返回0
            }
        }
        
        return record[s.size()];
    }
};

LeetCode198

入室抢劫(LeetCode198: House Robber)

给一个数组,数组中每个数字代表一个房间所拥有的钱的数量,现在一个抢劫犯要到房间里面抢钱,但相邻的房间会有警报器,改抢劫犯为了不触发警报器,应该如何行动,抢到最大数量的钱。

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
             Total amount you can rob = 1 + 3 = 4.

Input: [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
             Total amount you can rob = 2 + 9 + 1 = 12.

思路:动态规划

代码实现:

class Solution {
public:
    int rob(vector<int>& nums) {
        vector<int> record;
        if(nums.empty()){
            return 0;
        }
        if(nums.size() == 1){
            return nums[0];
        }
        
        if(nums.size() == 2){
            return nums[0]>nums[1]?nums[0]:nums[1];
        }
        
        record.push_back(nums[0]);
        record.push_back(nums[0]>nums[1]?nums[0]:nums[1]);
        
        for(int i = 2;i<nums.size();i++){
                record.push_back((record[i-2]+nums[i])>record[i-1]?(record[i-2]+nums[i]):record[i-1]);
        }
        
        return record.back();
    }
};

打家劫舍II

OJ链接

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

解答

前一题可以求出从房子0~i中能偷窃到的最高金额。这一题不同的是,最后一个房子和第一个房子相连,房子呈环状分布。因此,在这种场景下,最后一个房子和第一个房子也和其它相邻的房子一样存在约束。即:

  • 如果盗窃了最后一个房子,那么不能盗窃第一个房子
  • 如果盗窃了第一个房子,那么不能盗窃最后一个房子

如果能将环打破,那么就可以通过前一题求解。对于任一两个相邻的房子i和i+1。我们可以将问题分解成以下2个问题:

  • 不偷窃房子i,可以偷窃(可以不代表一定要偷窃)房子i+1
  • 不偷窃房子i+1,可以偷窃(可以不代表一定要偷窃)房子i

最终解由上面2个问题解的较大者,而上面2个问题都将环打破,因此可以使用前一题的解法分别求出上面2个问题的解,然后进一步求出最终结果

下面的代码i和i+1分别对应最后一个房子和第一个房子

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1)    return nums[0];
        return max(rob(nums,0,nums.size() - 2),rob(nums,1,nums.size() - 1));
    }
private:
    int rob(vector<int> &nums,int i,int j){
        if(i > j)   return 0;
        //dp[i] = max{dp[i - 1],nums[i] + dp[i - 2]}
        int include = nums[i],exclude = 0,res = include;
        for(int m = i + 1;m <= j;m++){
            int tp = max(include,nums[m] + exclude);
            if(tp > res)    res = tp;
            exclude = include;
            include = tp;
        }
        return res;
    }
};



打家劫舍III

OJ链接

小偷又发现一个新的可行窃的地点。 这个地区只有一个入口,称为“根”。 除了根部之外,每栋房子有且只有一个父房子。 一番侦察之后,聪明的小偷意识到“这个地方的所有房屋形成了一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

在不触动警报的情况下,计算小偷一晚能盗取的最高金额。

示例 1:

     3
    / \
   2   3
    \   \ 
     3   1

能盗取的最高金额 = 3 + 3 + 1 = 7.

示例 2:

     3
    / \
   4   5
  / \   \ 
 1   3   1

能盗取的最高金额 = 4 + 5 = 9.

解答

假设func(root)为问题的解,即能从以root为根节点的树中盗窃到的最大金额。那么以root房子开始分析,可以选择偷窃root房子,也可以选择不偷窃

  • 如果偷窃root房子,那么func(root) = root->val + func(root->left->left) + func(root->left->right) + func(root->right->left) + func(root->right->right)
  • 如果不偷窃root房子,那么func(root) = func(root->left) + func(root->right)

最终结果就是上面2这的较大者

/**
 * 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 {
public:
    int rob(TreeNode* root) {
        if(!root)   return 0;
        
        pair<int,int> res = robCore(root);
        return res.first;
    }
private:
    pair<int,int> robCore(TreeNode* root){
        if(!root)   return pair<int,int>(0,0);
        
        //first:可能偷窃子树的根节点;second:不偷窃子树的根节点
        pair<int,int> left = robCore(root->left);
        pair<int,int> right = robCore(root->right);
        
        int include = root->val + left.second + right.second;   //偷窃当前节点
        int exclude = left.first + right.first;                 //不偷窃当前节点
        
        return pair<int,int>(max(include,exclude),exclude);
    }
};

LeetCode139

单词分割I(LeetCode139:Word Break)

给一个长串的字符串,和一个字典数组,将该字符串分割成若干个单词,使得每个单词都可以在字典中找到。

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".


Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple".
             Note that you are allowed to reuse a dictionary word.


Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false

思路:动态规划,使用一个map结构纪录递归过程中,字符串的子串是否可以被成功分割。

递归过程:

  1. 分割点从字符串的首部开始,将字符串分为两部分:左子串和右子串
  2. 判断左子串能否被成功分割,再判断右子串是否存在于字典中。
  3. 只要当左子串能够被成功分割,且右子串存在与字典中,才说明字符串可以被成功分割
  4. 分割点不断向字符串尾部移动,其实就是在寻找最右边的第一个分割点。
  5. 所以左子串是一个由小到大的变化过程,所以使用map来纪录小规模的求解情况。

代码实现:


class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> dict_set(wordDict.begin(),wordDict.end());
        return wordBreak_x(s,dict_set);
            
    }
    
    bool wordBreak_x(string s,unordered_set<string>& dict_set){
        if(mem_.count(s)){
            return mem_[s];//如果记录中已经求解,则直接返回
        }
        
        if(dict_set.count(s)){
            return mem_[s] = true;//如果字典中存在,也直接返回
        }
        for(int i =0;i<= s.size();i++){
            string left(s.begin(),s.begin()+i);
            string right(s.begin()+i,s.end());
            if(dict_set.find(right)!=dict_set.end()){
                if(wordBreak_x(left,dict_set)){
                    return mem_[left] = true;//左子串可以分割,右子串存在于字典中,成功分割
                }
                
            }
        }
        return mem_[s]=false;//分割失败
    }
    
    private:
        unordered_map<string,bool> mem_; 
};

思路2:

对于字符串“leetcode”:

  • 如果字符串”l”存在于字典中,则字符串”leetcode”能否由字典中的单词构成取决于字符串“eetcode”能否由字典中的单词构成
  • 如果字符串“l”不在字典中,则判断字符串“le”是否字典中,如果在,则字符串“leetcode”能否由字典中的单词构成取决于字符串“etcode”能否由字典中的单词构成
  • 如果字符串“le”也不在字典中,那么判断字符串”lee”是否在字典中,如果在,则字符串“leetcode”能否由字典中的单词构成取决于字符串”tcode”能否由字典中的单词构成

令state[i]表示字符串中从下标i开始到结尾的子串能否由字典中的单词构成。对于”leetcode”:

  • state[0]就表示“leetcode”能否由字典中的单词构成
  • state[2]就表示“etcode”能否由字典中的单词构成
  • state[8]就表示”“能够由字典中的单词构成

假设substr(i,j)表示字符串s中下标i到下标j的子串,那么state[i] = dict.contain(substr(i,j)) && state[j + 1]

  • 时间复杂度:O(n^2)(n为字符串长度)
  • 空间复杂度:O(n + m)(n为字符串长度,m为字典中单词数)
```c++
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> dict(wordDict.begin(),wordDict.end());
        bool *state = new bool[s.size() + 1];
        for(int i = 0;i < s.size();i++) state[i] = false;
        state[s.size()] = true;
        
        for(int idx = s.size() - 1;idx >= 0;idx--){
            for(int len = 1;len <= s.size() - idx && !state[idx];len++)//注意这里循环结束的条件
                if(dict.find(s.substr(idx,len)) != dict.end())
                    state[idx] = state[idx + len];
        }
        
        return state[0];
    }
};

单词分割II(LeetCode140:Word BreakII)

题目同上,但是要将所有可能的分割情况输出

Input:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
Output:
[
  "cats and dog",
  "cat sand dog"
]

Input:
s = "pineapplepenapple"
wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
Output:
[
  "pine apple pen apple",
  "pineapple pen apple",
  "pine applepen apple"
]
Explanation: Note that you are allowed to reuse a dictionary word.

思路相同,不过涉及到对结果的处理,以及将分割成功的子串添加到结果集

  • 保存一个单词是否可以被分割的结论_mem
  • 若一个单词能被分割,需要保存分割字符串的结果_mem_map

代码:

class Solution {
public:
    vector<string> wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> dict_set(wordDict.begin(), wordDict.end());
        vector<string> result;
        wordBreak_x(result, s, dict_set);
        return result;
    }
    
    bool wordBreak_x(vector<string>& result, string s, unordered_set<string>& dict_set){
        if (s.empty()) {
            return true; 
        }
        //如果已经处理过该字符串,直接返回即可,不再重复判断
        if (_mem.count(s)) {
            if (_mem[s]) {
                result = _mem_map[s];
                return true;
            } else {
                return false;
            }
        }
        vector<string> result_tmp;
        for (int i = 0; i < s.size(); i++) {
            string left(s.begin(), s.begin() + i);
            string right(s.begin() + i, s.end());
            //如果右边字符串在字典中,则判断左边字符串能否分割
            if (dict_set.count(right)) {
                result_tmp.clear();
                if(wordBreak_x(result_tmp, left, dict_set)) {
                    //左边字符串可以被分割
                    if (result_tmp.empty()) {
                        result.push_back(right);
                    } else {
                        //将左边单词的分词结果和右边字符串进行拼接
                        for (int j = 0; j < result_tmp.size(); j++) {
                            result_tmp[j] += " " + right;
                        }
                        //将拼接好的当前分词结果添加到result中保存
                        result.insert(result.end(), result_tmp.begin(), result_tmp.end());
                    }
                }
            }
        }
        
        //保存当前单词的分词结果
        if (result.empty()) {
            _mem[s] = false;
            return false;
        } else {
            _mem[s] = true;
            _mem_map[s] = result;
            return true;
        }
    }
    private:
        //保留已经进行分割字符串的结果
        unordered_map<string, vector<string>> _mem_map;
        //保留当前单词是否可以被分割,防止相同的单词重复判断
        unordered_map<string, bool> _mem;
};

LeetCode238

剩余数组乘积(LeetCode238:Product of Array Except Self)

给出一个数组nums,给出当除掉nums[i]元素时,剩余元素的乘积。输出对每一个元素i的剩余乘积

Input:  [1,2,3,4]
Output: [24,12,8,6]

思路:动态规划

  1. 先求出以每个元素作为结尾的前面元素乘积record1
  2. 再求出以每个元素作为开头的后面元素的乘积record2
  3. 根据这两个记录信息求出所有除掉nums[i]的乘积

进阶:使用常量空间解决问题

  1. 将record1只用一个变量即可,存放到result中只有前半部分的乘积
  2. 再将record2也只需要一个变量记录,再遍历一次将后半部分的乘积和结果集中前半部分相乘即可

代码实现(只实现普通解法部分,进阶解法也很简单,这里不做实现)

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        vector<int> record1,record2,result;//record1和record2其实只用一个变量即可,因为一个记录用过一次就不会再使用
        if(nums.size()<=1) return result;
        record1.push_back(nums[0]);
        record2.push_back(nums[nums.size()-1]);
        int cur = 0;
        for(int i = 1;i<nums.size();i++){
            cur = nums[i]*record1[i-1];//记录前半部分的乘积
            record1.push_back(cur);
            cur = nums[nums.size()-1-i]*record2[i-1];//记录后半部分乘积
            record2.push_back(cur);
        }
        result.push_back(record2[nums.size()-2]);
         for(int i = 1;i<nums.size()-1;i++){
             result.push_back(record1[i-1]*record2[nums.size()-i-2]);//根据前半部分和后半部分来计算结果
         }
        result.push_back(record1[nums.size()-2]);
        
        return result;
        
    }
};

LeetCode279

完全平方和(LeetCode279:Perfect Squares)

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.

输入: n = 13
输出: 2
解释: 13 = 4 + 9.

思路参考

思路:

求一个数n最少由多少个完全平方数组成,假设q为sqrt(n):

  • 如果这些数里面包含1,那么求出n-1*1最少由多少个完美平方数组成,然后加1就是结果
  • 否则,如果这些数里面包含2,那么求出n-2*2最少由多少个完美平方数组成,然后加1就是结果
  • 否则,如果这些数里面包含q,那么求出n-q*q最少由多少个完美平方数组成,然后加1就是结果

因此,这是一个动态规划问题。F(n) = min{F(n),F(n-1)+1,F(n-4)+1,…,F(n-q*q)+1}。如果递归求解会存在重复子问题,因此使用一个数组state保存状态,“从小到大”求出F(1)到F(n),结果就是state[n] ,将state都初始化为INT_MAX

代码实现:

class Solution {
public:
    int numSquares(int n) {
        vector<int> state(n + 1, INT_MAX);//记录数据
        state[0] = 0;
        for (int i = 1; i <= n; ++i)
        {
            int q = (int)sqrt(i);//求可能的平方和范围
            //如果i刚好为完全平方数
            if (q*q == i) state[i] = 1;
            else
            {
                for (int j = 1; j <= q; ++j)
                    state[i] = min(state[i], state[i - j * j] + 1);
            }
        }
        return state[n];
    }
};

LeetCode300

最长升序序列(LeetCode300:Longest Increasing Subsequence)

给出一个未排序数组,给出该数组中最长的升序序列长度

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. 

要求:

时间复杂度保持在O(N^2)

进阶:

将时间复杂度降低为O(NlogN)

思路:动态规划,时间复杂度(O(N^2)),空间复杂度(O(N))

思路参考

  1. record记录以每个节点作为结尾的最长升序长度
  2. 其中的前后依赖关系为:当处理第n个元素时,比较前n个元素中所有小于n的元素的(最长序列长度+1)中的最大值,为当前n的最长序列长度
  3. 最后遍历该record数组,找到最长升序序列长度

思考:如何将该最长升序序列求出来

  1. 可以根据record记录回溯决策过程
  2. 先找到最长序列长度所在位置下标index
  3. 然后向前遍历,只要arr[i] < arr[index]且record[i] = record[index]-1就可以连接为一个最长升序序列。

代码实现:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.empty()) return 0;
        //用于记录前0~i-1中,nums[i]大于的数的state的最大值
        int maxOK = INT_MIN;
        vector<int> state(nums.size(),0);
        state[0] = 1;
        for(int i=1; i<nums.size(); ++i)
        {
            for(int j=0; j<i; ++j)
            {
                if(nums[i] > nums[j]) maxOK = max(maxOK,state[j]);
            }
            if(maxOK!=INT_MIN) state[i] = maxOK+1;
            else state[i] = 1;
            maxOK = INT_MIN;
        }
        int maxALL = INT_MIN;
        for(int i =0; i<state.size(); ++i)
            maxALL = max(maxALL, state[i]);
        return maxALL;
    }
};

思路3(非常精妙的想法):(当碰到一个破坏升序序列的元素时,将其替换掉该升序序列中第一个大于它的元素,这样可以维持之前的升序序列长度,同时还可以扩展之前的升序序列,只要最大元素不被替换)

使用一个数组S保存最长升序序列的状态,它始终保持升序,每个元素插入S中时,替换掉S中大于等于这个插入元素的第一个元素,如果插入的元素大于S的最后一个元素,那么扩展S

  • 其实该数组S中任意一个下标为i元素值为k的含义如下:

    • 所有长度为i的递增序列中最小的结尾数为k,因为这样我们可以判断遇到下一个元素时,是否可以扩展递增序列的长度。
    • 这里我们其实在S中可以使用二分查找,来定位S中的哪一个元素将被替换,或将S进行扩展。
  • 同样,如果想要求得最长递增序列,则可以在更新S的过程中,更新record[]数组,记录以每个元素结尾的递增序列的最长长度。
  • 然后根据record来回溯决策过程即可

举个例子:

nums = [5,6,7,1,2,8,3,4,0,5,9]

当处理到7时,因为前3个元素升序,所以组成一个升序序列:

S = [5,6,7]

当处理1时,它终止了序列持续上升的趋势,可能会引导出一个新的更长的升序序列。因此替换掉大于等于它的第一个元素5:

S = [1,6,7]

接着处理2:

S = [1,2,7]

处理8时,需要扩展升序序列:

S = [1,2,7,8]

然后处理3:

S = [1,2,3,8]

处理4:

S = [1,2,3,4]

处理最后3个元素:

S = [0,2,3,4,5,9]

因为只需遍历1遍数组,并且每次更新S可以使用二分查找(时间复杂度为O(logn)),对数组中每一个数进行一次二分查找,所以总时间复杂度为O(n*logn)

因为只需遍历一遍数组,所以可以直接在nums的前部进行修改,作为S,从而不需要额外的空间

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.empty()) return 0;
        //记录S最后一个元素的下一个位置,即S为[nums.begin(),end)
        auto end = nums.begin();
        for(auto n=nums.begin(); n!=nums.end(); ++n)
        {
            //lower_bound使用二分查找
            //获取[m.begin(),end)中大于n的第一个元素
            auto iter = lower_bound(nums.begin(),end,*n);
            *iter = *n;
            //如果n大于S中所有的数,则需要拓展S
            if(iter==end)
                end++;
        }
        return end - nums.begin();
    }
};

LeetCode322

最小硬币找零方案(LeetCode322:Coin Change)

给出一个数组,其中有所有硬币面值的数值,再给出一个数值,我们要根据给出的硬币面值,用最少数量的硬币凑成给定数值的零钱。

Input: coins = [1, 2, 5], amount = 11
Output: 3 
Explanation: 11 = 5 + 5 + 1

Input: coins = [2], amount = 3
Output: -1

思路:同完全平方和一样,使用动态规划,从0开始计算,一直计算到给定数值amount。其中状态转换方程为:record[n] = min{1+record[n-coins[j]]},j代表coins中所有小于n的面值下标

代码实现:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> record(amount+1);
        sort(coins.begin(),coins.end());//排序
        record.push_back(0);
        for(int i = 1;i<=amount;i++){
            record[i] = -1;
        }
        for (int i = 1;i<=amount;i++){//动态规划记录从0到amount的最小硬币凑零方案
            int tmpcount = 0;
            for (int j = 0;j<coins.size();j++){
                if(i>=coins[j]){//只计算小于amount的面值
                    if(record[i-coins[j]]!=-1){
                        if(record[i] == -1)
                            record[i] = record[i-coins[j]]+1;
                        else
                            record[i] = record[i]< record[i-coins[j]]+1?record[i]:record[i-coins[j]]+1;//取较小值
                    }
                }else{
                    break;//因为排过序,所以所有大于amount的面值不用考虑
                }
            }
        }
        
        return record[amount];
            
    }
};

ChangeMoney

换钱最少货币数

题目1:给定一个数组arr,该数组中有很多纸币的面值,现在要凑成指定总值sum的钞票,问最少可以用几张纸币凑成该总值sum,且每张面值的纸币不限数量

输入:arr = [5,2,3],sum = 20
输出:4(最少4张:5,5,5,5)

输入:arr = [3,5] sum = 2
输出:0,无法组成2元总值

思路:动态规划,构建二维数组dp[i][j]。(一维数组也可以实现。)

  1. i,j代表在面值arr[0~i]中获取总值为j的最少纸币张数为dp[i][j]
  2. 在求dp[i][j]时,我们需要从如下几种情况来获取最小值:
    • 不取用当前面额钞票arr[j],所以dp[i][j]的一个可能值为dp[i-1][j]
    • 取用一张,可能值为dp[i-1][j-arr[j]]
    • 取用两张,可能值为dp[i-1][j-arr[j]*2]
    • …如此直到arr[j]*n>j时停止。如此得到以上的所有可能值,然后去这些值中的最小值即可

题目2:同题目1,不过每张面值的钞票只有一张,具有数量限制

思路:同题目一,使用动态规划,但这里更简单,因为可能值只有两种:不取用,和只取一张,所以无论数量有什么限制,只不过在最小值的候选值中数量产生变化而已。

  • 可以只用一维数组即可

题目3:同题目1描述的场景,但是求有多少种换钱的方法。

  • 同样适用动态规划,将所有可能值加起来,当求dp[i][j]时,分为不取用arr[j],取用1张,2张..再将所有的方法数加起来。
  • 其实这些所有的不同的取用方法分为两类就够了:
    • 不取用:dp[i][j-1]
    • 取用:dp[i][j-arr[j]]//必然取用一个arr[j]面值钞票,然后其余的j-arr[j]依然用arr[0-i]来组成

LeetCode334

递增三元子序列(LeetCode334: Increasing Triplet Subsequence)

给出一个数组,求该数组中是否可以找到三个元素的子序列(不一定连续),保持递增关系。

Given [1, 2, 3, 4, 5],
return true.

Given [5, 4, 3, 2, 1],
return false.

思路:可以参考LeetCode300:最长升序子序列

维护一个二元数组,参考LeetCode300中的思路三。也可以使用动态规划来求解这题

代码实现:

class Solution {
public:
    bool increasingTriplet(vector<int>& nums) {
        if(nums.size()<3){
            return false;
        }
        int *record = new int[2];
        for (int i = 0;i<2;i++){
            record[i] = INT_MAX;
        }
        bool res = true;
        for(int i = 0;i<nums.size();i++){
            res = true;
            for(int j = 0;j<2;j++){
                if(record[j]>=nums[i]){//将当前遍历到的元素和二元数组中的元素逐个比较,若二元数组中的元素大于该元素,则替换,直到当二元数组的元素均小于当前元素时,说明出现三元递增序列
                    res = false;
                    record[j] = nums[i];
                    break;
                }
            }
            if(res)
                return res;
        }
        return res;
    }
};

LeetCode32

最长括号长度(LeetCode32:Longest Valid Parentheses)

给出一个括号组成的字符串,问该字符串中有效括号的最长长度为多少?

Input: "(()"
Output: 2
Explanation: The longest valid parentheses substring is "()"


Input: ")()())"
Output: 4
Explanation: The longest valid parentheses substring is "()()"

思路:利用栈+动态规划解决

  1. 利用栈记录每个括号的所在位置索引
  2. 当一个”)”和栈顶的”(“构成一个有效对时,利用下标计算其中的有效长度,并将栈顶的”(“弹出
  3. 利用一维数组record记录以每个下标元素结尾的最长有效括号长度。
  4. 所以当遍历到”)”,和栈顶的”(“的下标之差再加上”(“前一个元素的有效长度,即为以当前的“)”结尾的最长有效长度:record[i] = i-index+1+record[index-1]

代码实现:

class Solution {
public:
    int longestValidParentheses(string s) {
        if(s.empty())
            return 0;
        stack<pair<char,int>> sta;
        int record[s.size()] = {0};
        int maxvalid = 0;
        for(int i =0;i<s.size();i++){
            if(s[i] == '('){//遍历到"(",则有效长度为0,因为不存在以"("结尾的有效括号组合
                sta.push(pair<char,int>('(',i));
                record[i] = 0;
            }else{
                if(!sta.empty()){
                    if(sta.top().first == '('){
                        int index = sta.top().second;//计算和当前“)”构成有效括号的"("的下标
                        if(index > 0)
                            record[i] = i-index + 1 + record[index-1];
                        else
                            record[i] = i-index + 1;
                    maxvalid = maxvalid>record[i]?maxvalid:record[i];//更新最长有效长度
                    sta.pop();
                    }
                }else
                    record[i] = 0;
                
            }
        }
        
        return maxvalid;
    }
};

LongestCommonSubsequence

最长公共子序列

给定两个字符串str1和str2,返回两个字符串的最长公共子序列

输入:str1 = "1A2C3D4B56"   str2 = "B1D23CA45B6A"

"123456"和"12C4B6"都是最长公子序列。返回哪一个都行

思路:动态规划,dp[i][j]代表在str1[0~i]和str2[0~j]的两个子串中的最长公共子序列的长度

  1. 初始化
  2. dp[i][j]有三个可能值
    • dp[i][j-1],最长公共子序列与str2[j]无关
    • dp[i-1][j],最长公共子序列与str1[i]无关
    • dp[i-1][j-1]+1只有当str1[i] == str2[j]时,最长公共子序列的长度才可能与两个元素都有关系
    • 所以取上述三个值中的最大值作为dp[i][j]的值
  3. 如何根据dp[i][j]来得到最长公共子序列的内容?从dp右下角元素向dp[0][0]移动,求出该长度为dp[len1][len2]的最长公共子序列
    • 可以根据dp[i][j]和dp[i][j-1]的关系判断str2[j]是否被使用,如果相等说明没有被使用,否则str2[j]参与了该最长公共子序列
    • 同理根据dp[i][j]和dp[i-1][j]判断str1[i]是否被使用。
    • 如果dp[i][j]和dp[i][j-1]及dp[i-1][j]同时相等,说明都没有被使用
    • 如果dp[i][j]同时大于dp[i][j-1]及dp[i-1][j],则说明都被使用,且str1[i] == str[2]。

LongestCommomSubstring

最长公共子串

给出两个字符串str1,str2返回该两个字符串的最长公共子串

str1 = "1AB2345CD",str2 = "12345EF"

返回“2345”

要求:如果str1长度为M,str2长度为N,实现时间复杂度为O(M*N),额外空间复杂度为O(1)

思路:动态规划,此时dp[i][j]的含义为以str1[i]和str2[j]结尾的最长公共子串

dp[i][j]只有两种可能:

  1. 当str1[i] == str2[j]时,dp[i][j] = dp[i-1][j-1]+1
  2. 否则str1[i] != str2[j]时,dp[i][j] = 0;

如何优化使得空间复杂度只有O(1)?

由于在计算dp[i][j]时只需要dp[i-1][j-1],在dp的二维数组中,相当于一条斜线可以全部求出来,然后遍历所有的斜线即可一共有M+N-1条。都只使用一个变量记录即可。

MinEditCost

最小编辑代价

给定两个字符串str1和str2,再给定三个整数ic,dc,rc代表插入,删除和替换一个字符的代价,返回将str1编辑成str2的最小代价

输入:str1 = "abc",str2 = "adc",ic = 5,dc = 3,rc = 2。
输出:2,从"abc"编辑成"adc",把"b"替换成"d"的代价最小,返回2

思路:动态规划

  • 其实某个问题可以分析出以哪些子问题解决的情况下为基础,然后经过进一步操作解决都可以进行动态规划
  • 根据题目需求,在选取这些所有可能的解中最能满足题目要求的解即可
  • 对这个题目进行分析,由于只有三个操作,所以我们分析有四种可能值
      1. str1[0~i] 到str2[0~j]最后一个字符str2[j]经过删除得到
      1. str1[0~i] 到str2[0~j]最后一个字符str2[j]经过插入得到
      1. 如果str1[i] != str2[j],str1[0~i] 到str2[0~j]最后一个字符str2[j]经过替换得到
      1. 如果str1[i] == str2[j],无需替换,最小操作数和str1[0~i-1]转换为str2[0~j-1]一样,最后两个字符无需任何操作。
  • 所以从这四个可能的值中,找出题目所要求的值,最少需要的操作数取这四个值中的最小值即可

动态规划优化:空间压缩,由于dp[i][j]需要参考的子问题值包括dp[i-1][j],dp[i][j-1],dp[i-1][j-1],要使用一维数组+一个变量保存dp[i-1][j-1]的值即可完成动态规划过程。

代码实现:


class Solution {
public:
    int getMinEdit(string str1, string str2) {
        int **dp = new int*[str1.length()+1];
        for (int i = 0; i < str1.length() + 1; i++) {
            dp[i] = new int[str2.length()+1];
        }

        int dc = 3, ic = 5, rc = 2;//代表各操作代价:删除,插入,替换
        for (int i = 0; i < str1.length() + 1; i++) {
            dp[i][0] = dc * i;//str1变为空串,只能通过删除来实现
        }

        for (int j = 0; j < str2.length() + 1; j++) {
            dp[0][j] = ic * j;//str1为空串变为str2只能通过插入来实现
        }

        int delete_op,insert_op,replace_op;
        for (int i = 1; i < str1.length() + 1; i++) {
            for (int j = 1; j < str2.length() + 1; j++) {
                delete_op = dp[i - 1][j] + dc;//先将str1[0~i-2]转换成str2[0~j-1],最后删除str1[i-1]的最小操作数
                insert_op = dp[i][j - 1] + ic;//先将str1[0~i-1]转换成str2[0~j-2],最后插入str2[j-1]转换得到的最小操作数
                replace_op = str1[i - 1] == str2[j - 1] ? dp[i - 1][j - 1] : dp[i - 1][j - 1] + rc;//相等不用替换,不相等,则先将str1[0~i-2]转换成str2[0~j-2],然后将str1[i-1]替换为str2[j-1]的最小操作数
                dp[i][j] = min(min(delete_op, insert_op), replace_op);
            }
        }
        int res = dp[str1.length()][str2.length()];
        for (int i = 0; i < str1.length() + 1; i++) {
            delete [] dp[i];
        }
        delete[] dp;
        return res;
    }

};

ExpressTDesired

表达式得到期望结果的组成种数

给定一个只由0(假),1(真),&(与),|(或),^(异或)组成的表达式express,再给定一个布尔值desired。返回express能有多少种组合方式,可以达到desired的结果

输入:express = "1^0|0|1",desired = false

输出:2,有两种:“1^((0|0)|1)”和1^(0|(0|1))

思路:递归

  1. 尝试从左到右将式子分为两个部分left和right,根据期望值desired和划分符号:&, ,^来得到所有可能的组合总数
  2. desired为true时
    • 划分符号为&,递归求左边为真的总数*右边为真的总数
    • 划分符号为|,递归求left为真right为假+left为假right为真+left为真*right为真
    • 划分符号为^,递归求left为真right为假+left为假right为真
  3. desired为false时
    • 划分符号为&,递归求left为真right为假+left为假right为真+left为真*right为真
    • 划分符号为|,递归求左边为假的总数*右边为假的总数,
    • 划分符号为^,递归求left为假right为假+left为真right为真

优化: 动态规划,时间复杂度O(N^3),空间复杂度为O(N^2)

  • 发现在递归过程中我们涉及到大量的重复计算,所以我们用两个数组
    • t[i][j]:表示express下标为i到下标为j地方的组合为true的总数
    • f[i][j]:表示express下标为i到下标为j地方的组合为false的总数
    • 当我们求解t[i][j]和f[i][j]时,同样需要将i~j之间的字符串逐个进行划分枚举然后计算所有组合总数O(N^2)*O(N)
    • 这里两个规划数组,我们相当于求解了两个目标问题:目标为true和目标为false

CardGame

纸牌博弈问题

给定一个整型数组,代表数值不同的纸牌排成一列,玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿。但每个玩家只能拿最左或最右的纸牌,玩家A和玩家B都决定聪明,请返回最后获胜者分数。

输入:arr = [1,2,100,4]
输出:101,B会获胜

思路:递归,我们需要定义两个函数:f,s。

  • f表示玩家在一个列纸牌中先拿可以拿到的最大值:max(arr[i]+s(i+1,j),arr[j]+s[i,j-1]);(拿两边的任意一张可以获取的最大值)
  • s表示绝顶聪明的对手玩家可以拿得到的值,绝顶聪明的对手玩家会让玩家拿最少的点数:min(f(i+1,j),f(i,j-1))(对手会让玩家拿最少的点数)。

在这个计算过程中

  • 计算f(i,j)时会有两个递归分叉:s(i+1,j),s(i,j-1)
  • 计算s(i,j)时也会有两个递归分叉:f(i+1,j),f(i,j-1)
  • 所有其中会涉及到很多重复计算,我们需要将f(i,j)和s(i,j)的结果记录下来,动态规划,减少不必要的重复计算
  • f[i][j]:代表玩家先拿时,可以得到最大点数
  • s[i][j]:代表玩家后拿时,被动可以拿到的最少点数,因为对手会尽可能的拿到最多点数

PS:动态规划初始化时,根据求值顺序的规律,我们应先初始化所有的frecord[i][i],然后开始动态规划

代码实现:

class Solution {
public:
    int getWinScore(vector<int> arr) {
        if (arr.empty())
            return 0;
        vector<vector<int>> frecord,srecord;
        for (int i = 0; i < arr.size(); i++) {
            frecord.push_back(vector<int>(arr.size(),0));
            srecord.push_back(vector<int>(arr.size(), 0));
        }
        //初始化

        for (int i = 0; i < arr.size(); i++) {
            frecord[i][i] = arr[i];//初始化二维数组斜线上record[i][i]的值
        }

        for (int i = arr.size() - 2; i >= 0; i--) {
            for (int j = i + 1; j < arr.size(); j++) {
                frecord[i][j] = max(arr[i] + srecord[i+1][j],arr[j]+srecord[i][j-1]);//开始动态规划,求先拿的最大点数
                srecord[i][j] = min(frecord[i + 1][j], frecord[i][j - 1]);//求后拿得到的点数
            }
        }

        return max(frecord[0][arr.size() - 1], srecord[0][arr.size() - 1]);//返回先拿后拿得到的点数较大者
    }

};

code269

添加最少字符串使字符串整体都是回文字符串

给定一个字符串,如果可以在str的任意位置添加字符串,请返回在添加字符串最少的情况下,让str整体都是回文字符串的一种结果。

输入:“ABC”
输出:"ABCBA"

思路:动态规划,二维数组dp[i][j]

  • dp[i][j]的含义:str[i][j]变为回文字符串的最少字符添加次数
  • 初始化:所有长度为1的子字符串的最少添加次数为0
  • 所有长度为2的子字符串的最少添加次数:若两个字符相等,则为0,若不相等,则为1
  • dp[i][j] 有如下几种可能的值
    • 如果str[i] == str[j],则dp[i][j] == dp[i-1][j-1]
    • 如果不等,取下面两种值的较小值min
      • 首先将str[i+1][j]变为回文串,然后在右边添加字符str[i]构成回文串:次数为dp[i+1][j] + 1
      • 首先将str[i][j-1]变为回文串,然后在左边添加字符str[j]构成回文串:次数为dp[i][j-1] + 1

根据dp[][]数组回溯决策过程

  • 首先根据dp[0][size-1]的值(字符添加次数)+ str.size,构建添加字符后的回文字符串容器res
  • 根据字符串的两端str[i]和str[j]
    • 如果相等,则容器两端均添加str[i]
    • 如果不等,则比较dp[i+1][j]和dp[i][j-1]
      • 若dp[i+1][j]较小,则res两端添加str[i]
      • 若dp[i][j-1]较小,则res两端添加str[j]
  • 最后返回res即可

代码实现:

class Solution {
public:
    string getPanlindromel(string str) {
        if (str.empty())
            return NULL;
        int **dp = new int*[str.size()];
        for (int i = 0; i < str.size(); i++) {
            dp[i] = new int[str.size()];
        }
        //初始化所有长度为1的字符串次数为0
        //初始化所有长度为2的字符串次数要么为0,要么为1
        for (int i = 0; i < str.size(); i++) {
            dp[i][i] = 0;
        }

        for (int i = 0; i < str.size()-1; i++) {
            if (str[i] == str[i + 1])
                dp[i][i + 1] = 0;
            else
                dp[i][i + 1] = 1;
        }

        for (int i = str.size() - 3; i >=0 ; i--) {
            for (int j = i+2; j < str.size(); j++) {
                if (str[i] == str[j])
                    dp[i][j] = dp[i + 1][j - 1];
                else
                    dp[i][j] = min(dp[i+1][j],dp[i][j-1])+1;
            }
        }

        //二维数组构造完毕
        //回溯得到可能的回文串
        string res(str.size()+dp[0][str.size()-1],0);//创建一个可以容纳回文串长度的字符串
        int i = 0,j=str.size()-1;//从两边构造该回文串
        int left = 0, right = str.size() + dp[0][str.size() - 1] - 1;
        while (left<=right) {
            if (str[i] == str[j]) {//两边相等不用添加
                res[left++] = str[i];
                res[right--] = str[j];
                i++;
                j--;
            }
            else if(dp[i + 1][j] < dp[i][j - 1]) {//说明从右边添加str[i]的添加字符次数会少一点
                res[left++] = str[i];
                res[right--] = str[i];
                i++;
            }
            else {//说明从左边添加字符str[j]的添加字符次数会少一点
                res[left++] = str[j];
                res[right--] = str[j];
                j--;
            }
        }

        return res;


    }


};

LeetCode647

回文子串个数统计(LeetCode647:Palindromic Substrings)

给出一个字符串,求该字符串子串中有多少回文字符串?即使两个不同下标的字符有相同的字符也算两个不同的回文字符串

输入:"abc"
输出:3  "a" "b"  "c"

输入:"aaa"
输出:6  "a" "a" "a" "aa" "aa" "aaa"

思路1:动态规划

  1. dp[i][j]代表字符串子串str[i~j]是否为回文串
  2. 如果str[i] == str[j] :则dp[i][j] = dp[i+1][j-1];
  3. 如果str[i] != str[j] :则dp[i][j] = 0;
  4. 在计算dp的过程中统计回文串的个数

代码实现:

class Solution {
public:
    int countSubstrings(string s) {
        if(s.empty()){
            return 0;
        }
        if(s.size()==1)
            return 1;
        if(s.size()==2){
            if(s[0]==s[1])
                return 3;
            else
                return 2;
        }
        const int len = s.size();
        int dp[len][len] = {0};//二维数组的初始化!!!!
        int count = 0;
        //初始化
        for(int i = 0;i<s.size();i++){
            dp[i][i] = 1;
            count++;
            if(i<s.size()-1&&s[i] == s[i+1]){
                dp[i][i+1] = 1;
                count++;
            }
        }
        
        //状态转换
        for(int i = s.size()-3;i>=0;i--){
            for(int j = i+2;j<s.size();j++){
                if(s[i] == s[j]){
                    dp[i][j] = dp[i+1][j-1];
                    count += dp[i][j];
                }   
            }
        }
        
        return count;
    }
};

思路2:扩展

  1. 遍历每一个字符,以每个字符为中心进行扩张
    • 扩展方式有两种
      • 单个字符为中心扩展
      • 两个相同字符为中心扩展
  2. 扩展过程中统计回文子串的个数

代码实现:

    int countSubstrings(string s) {
        int res = 0, n = s.length();
        for(int i = 0; i < n; i++){//遍历每一个字符
            for(int j = 0; i-j >= 0 && i+j < n && s[i-j] == s[i+j]; j++)res++; //substring s[i-j, ..., i+j],单个字符为中心扩展
            for(int j = 0; i-1-j >= 0 && i+j < n && s[i-1-j] == s[i+j]; j++)res++; //substring s[i-1-j, ..., i+j],两个相同字符为中心扩展
        }
        return res;
    }

code422

正数数组的最小不可组成和

给定一个正数数组,以下是最小不可组成和的概念:

  • 把arr每个子集内的所有元素加起来会出现很多值,其中最小的记为min,最大的记为max
  • 在区间[min,max]上,如果有数不可以被arr某一个子集相加得到,那么其中最小的那个数是arr的最小不可组成和。
  • 在区间[min,max]上,如果所有的数都可以被arr的某一个子集相加得到,那么max+1是arr的最小不可组成和

请写函数返回正数数组arr的最小不可组成和。

输入:arr=[3,2,5]
输出:4  (子集{2}相加得到最小值min = 2,子集{3,2,5}相加得到最大值max = 10,在2~10 中最小不可组成和为4)

输入:arr=[1,2,4]
输出:8

思路1:O(2^N),暴力递归,先将所有两个数相加的和保存到哈希表,然后找到其中最小的不可组成和。 思路2:O(N^2)

  • 动态规划,一维数组dp,长度为sum+1,sum为数组中所有元素的累加和,范围为[0,sum],dp[i]=true表示i可以被子集累加得到。
  • 然后遍历arr,从arr[0]遍历到arr[0~n-1],当arr[0~i]更新到arr[0~i+1],需要更新一次dp[],更新0~sum是否可以被子集累加得到
  • 所以更新一次的复杂度为O(sum),需要更新N次,所以一共的复杂度为O(N*sum)

进阶问题:如果正数数组arr中肯定有1这个数,求最小不可组成和,要求时间复杂度为O(NlogN),空间复杂度为O(1)

  • 先将arr[0~n-1]进行排序,必有arr[0]==1,从左向右进行遍历,range代表当计算到arr[i]时,[1,range]区间内的所有正数都可以被arr[0~i-1]的某个子集累加和得到。
  • 所以如果arr[i]>range+1,因为arr[0~i-1]累加数的范围只能是[0,range], arr[i]+任意arr[0~i-1]的累加和的最小也只能是range+2,所以中间的range+1是无法累加得到的,所以直接返回range+1
  • 当arr[i]<=range+1,说明[1,range+arr[i]]区间上的所有正数都可以被arr[0~i]累加得到,所以另range = range+arr[i];

动态规划ending

递归

LeetCode17

手机号码的字符串组合(LeetCode17:Letter Combinations of a Phone Numbe)

按手机输入法的格式,输入一串数字,得到所有可能的字符串: leetcode_phonenum.png

简单的递归思路:需要注意的点就是传入参数时,传入引用可以节省大部分运行时间

代码:

class Solution {
public:
    map<int,string> num_map = { {2,"abc"},{3,"def"},{4,"ghi"},{5,"jkl"},{6,"mno"},{7,"pqrs"},{8,"tuv"},{9,"wxyz"} };//关联容器的初始化
    
    vector<string> letterCombinations(string digits) {
        string str;
        vector<string> vs;
        if(digits.size() == 0)
            return vs;
        vs = letterCombinations_x(digits,vs,str,0,digits.size());
        return vs;
    }
    vector<string> &letterCombinations_x(string &digits,vector<string> & vs,string &str,int n,int size) {
        if(n == size){
            vs.push_back(str);
            return vs;
        }
        string num_str = num_map[digits[n]-'0'];
        for(int i=0;i<num_str.size();i++){
            string str_tmp = str;
            str_tmp.push_back(num_str[i]);
            letterCombinations_x(digits,vs,str_tmp,n+1,size);//开始递归
        }
        return vs;
    }
};

LeetCode46

排列组合(LeetCode 46: Permutations)

给出一个整形数组,输出该整形数组的排列组合:

思路:将首元素和所有的元素(包括它自己)进行交换后,递归求解剩余元素的排列组合,具体思路可以见剑指offer的面试题38,字符串的排列组合。

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
         vector<vector<int>> result;
        if(nums.empty()){
            return result;
        }
        int start = 0,end = nums.size()-1;
        permute_x(start,end,nums,result);
        return result;
        
    }
    
    void permute_x(int start,int end,vector<int>& vec,vector<vector<int>>& result ){
        if(start == end){
            result.push_back(vec);
            return;
        }
        
        for(int i = start;i<=end;i++){
            vector<int> vec1 = vec;
            int tmp = vec1[start];//首元素和其余元素交换后
            vec1[start] = vec1[i];
            vec1[i] = tmp;
            permute_x(start+1,end,vec1,result);//递归进行排列组合
        }
        
        return;
        
    }
    
};

LeetCode48

n x n矩阵的旋转(LeetCode48: Rotate Image)

给一个nxn矩阵matrix,输出它向右旋转90度的新的nxn矩阵:

如input matrix = 
[
  [1,2,3],
  [4,5,6],
  [7,8,9]
]
输出:
[
  [7,4,1],
  [8,5,2],
  [9,6,3]
]

思路:将外围的部分进行旋转后,剩下的部分依然是一个(n-2)x(n-2)的矩阵,递归求解

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        if(matrix.empty()){
            return;
        }
        int size = matrix[0].size();
        rotate_x(matrix,size,size);
    }
    
    void rotate_x(vector<vector<int>>& matrix,int n,int size){
        if(n==0||n==1){
            return;
        }
        int start = (size-n)/2;
        int end = start+n-1;
        //将外围进行旋转操作
        for(int i = 0;i<n-1;i++){
            int tmp = matrix[start][start+i];
            matrix[start][start+i] = matrix[end-i][start];
            matrix[end-i][start] = matrix[end][end-i];
            matrix[end][end-i] = matrix[start+i][end];
            matrix[start+i][end] = tmp;
        }
        //递归求解更小规模的矩阵
        rotate_x(matrix,n-2,size);
    }
};

LeetCode54

旋转打印矩阵(LeetCode54:Spiral Matrix)

给一个mxn二维矩阵,顺时针的顺序将所有元素打印出来:

Input:
[
 [ 1, 2, 3 ],
 [ 4, 5, 6 ],
 [ 7, 8, 9 ]
]
Output: [1,2,3,6,9,8,7,4,5]

Input:
[
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9,10,11,12]
]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

思路:递归,每次打印分为四个步骤:横向,纵向,再横向逆序,纵向逆序,然后递归打印剩余部分,依然是一个矩形。

注意:需要考虑最小问题的求解

  1. 高为1的矩形打印,只需要一部
  2. 长度为1的矩形打印,只需要一步
  3. 高为2的矩形打印,只需要三部

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> result;
        if(matrix.empty())
            return result;
        int height = matrix.size()-1;
        int len = matrix[0].size()-1;
        spiralOrder_x(matrix,0,len,height,result);
        return result;
        
    }
    
    void spiralOrder_x(vector<vector<int>>& matrix , int n, int len, int height , vector<int>& result){
        
        if(n>len/2 || n>height/2)
            return;
        int start_i = n,start_j = n,end_i = height - n,end_j = len - n;
        if((height - 2*n + 1)==1){//子问题1
            for(int j = start_j;j<=end_j;j++)
                result.push_back(matrix[start_i][j]);
            return;
        }
        
        if((len -2*n + 1) == 1){//子问题2
            for(int i = start_i;i<=end_i;i++){
                result.push_back(matrix[i][end_j]);
            }
            return;
        }
        
        if((height-2*n +1) == 2){//子问题3
            for(int j =start_j;j<=end_j;j++)
                result.push_back(matrix[start_i][j]);
            result.push_back(matrix[start_i+1][end_j]);
            for(int j = end_j-1;j>=start_j;j--)
                result.push_back(matrix[end_i][j]);
            return;
        }
        //常规四步打印
        for(int j = start_j;j<=end_j;j++)
                result.push_back(matrix[start_i][j]);
        for(int i = start_i+1;i<=end_i;i++)
            result.push_back(matrix[i][end_j]);
        for(int j = end_j-1;j>=start_j;j--)
            result.push_back(matrix[end_i][j]);
        for(int i = end_i-1;i>start_i;i--)
            result.push_back(matrix[i][start_j]);
        
        spiralOrder_x(matrix,n+1,len,height,result);//递归打印小规模矩形
        
    }
};

LeetCode78

求数组所有可能的子集(LeetCode78:Subsets)

给出一个没有重复元素的数组,输出它所有可能的子集:

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

递归:处理第一个元素,存在和不存在两种情况,如此循环处理数组的每一个元素


class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> empty;
        if(nums.empty()){
            result.push_back(empty);
            return result;
        }
        int start = 0,end = nums.size();
        subsets_x(nums,start,end,result,empty);//从头开始遍历
        return result;
        
    }
    
    void subsets_x(vector<int>& nums,int start,int end,vector<vector<int>>& result,vector<int> cur){
        if(start == end){
            result.push_back(cur);
            return;
        }
        subsets_x(nums,start+1,end,result,cur);//当前元素在该子集中的情况
        cur.push_back(nums[start]);
        subsets_x(nums,start+1,end,result,cur);//当前元素不在子集中的情况
        return;
    }
};

含重复元素集合的所有子集

OJ链接

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]
解答

为了方便处理相同元素,先排序

考虑[1,2,2,3],假设对于每个元素还是有2种选择:要或者不要,那么在处理数字2时就会出现重复:

  • 包含第1个2,不包含第2个2
  • 不包含第1个2,包含第2个2

因此需要防止重复,也就是在DFS到第1个2时,应该怎么处理?

  • 首先将2添加到路径中,然后DFS后一个元素(在这里就是第2个2
  • DFS返回后,将第1个2从路径中删除
  • 如果此时DFS后一个元素(在这里就是第2个2),那么就会和第一次DFS产生冗余。因为只要包含相同元素,那么选择不要相同元素的第1个元素肯定会和选择要相同元素的第1个元素,而不要其后的某个元素产生重复。因此,每次将一个元素从路径中删除时,应该跳到后面第一个不相等的元素进行DFS
class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        
        vector<vector<int>> res;
        vector<int> path;
        
        dfs(nums,0,res,path);
        
        return res;
    }
private:
    void dfs(vector<int> &nums,int idx,vector<vector<int>> &res,vector<int> &path){
        if(idx == nums.size()){
            res.push_back(path);
            return;
        }
        
        path.push_back(nums[idx]);
        dfs(nums,idx + 1,res,path);
        path.pop_back();
        //跳过相同的元素
        while(idx + 1 < nums.size() && nums[idx] == nums[idx + 1])  idx++;
        dfs(nums,idx + 1,res,path);  
    }
};

LeetCode79

给出一个二维数组,和一个匹配字符串,求得该二维数组中有没有相连(上下左右)的字符和所给的字符串匹配。相连的字符不可以重复使用。

board =
[
  ['A'->,'B'->,'C',  'E'],
  ['S',  'F',  |'C',  'S'],
  ['A',  'D',<-|'E',  'E']
]

Given word = "ABCCED", return true.
Given word = "SEE", return true.
Given word = "ABCB", return false.

思路:先匹配第一个字符,然后递归匹配“匹配字符串”的其它字符,并且创建一个visited数组记录匹配过程中是否被访问,防治重复访问。


class Solution {
public:
    bool exist(vector<vector<char>>& board, string word) {
        if(board.empty()){
            if(word.empty())
            return true;
            return false;
        }
        bool result = false;
        int col = board.size();
        int raw = board[0].size();
        int **visited = new int*[col];
        for(int i = 0;i<col;i++){
            visited[i] = new int[raw];
        }
        
        for (int i = 0 ;i< col;i++){
            for (int j = 0 ; j < raw ; j++){
                visited[i][j] = 0;//记录是否被访问
            }
        }
        
        
        
        
        for (int i = 0 ;i< col;i++){
            for (int j = 0 ; j < raw ; j++){
                if(board[i][j] == word[0]){//先找到第一个匹配的字符
                    result = match(board,visited,word,0,i,j,word.size(),col,raw);
                    if(result)
                    return result;
                }
            }
        }
        
        return result;
    }
    
    bool match(vector<vector<char>>& board, int** visited ,string& word,int k, int i, int j,int size,int col, int raw){
        bool success = false;
        if(board[i][j] == word[k] ){
            visited[i][j] = 1;
            if(k == size-1){
                return true;
            }
            //四个方向进行回溯
            if(i<col-1 && !visited[i+1][j])
                success = match(board,visited,word,k+1,i+1,j,size,col,raw);
            if(!success && j<raw-1 && !visited[i][j+1])
                success = match(board,visited,word,k+1,i,j+1,size,col,raw);
            if(!success && i>0 && !visited[i-1][j])
                success = match(board,visited,word,k+1,i-1,j,size,col,raw);
            if(!success && j>0 && !visited[i][j-1])
                success = match(board,visited,word,k+1,i,j-1,size,col,raw);
            if(success)
                return success;
            else{
                visited[i][j] = 0;//如果匹配失败,消除访问记录
                return success;
            }
        }
        else
        return success;
    }
};

LeetCode131

回文分割(LeetCode131:Palindrome Partitioning)

给出一个字符串,给出该字符串所有可能的分割情况,使得每一个子串都是回文:

Input: "aab"
Output:
[
  ["aa","b"],
  ["a","a","b"]
]
  • 思路:回文结构一共只有两种情况
    1. 以单个字符为中心
    2. 以两个相同的字符为中心
  • 所以遍历该数组,尝试以每一个字符为中心进行回文合并
  • 并测试是否有连续两个相同的字符,并以该两个相同的字符进行合并

代码实现(代码优化):


class Solution {
public:
    vector<vector<string>> partition(string s) {
        vector<vector<string>> result;
        vector<string> start;
        
        for(auto c:s){
            char s[2] = {c, 0};
            start.push_back(s);
        }
        result.push_back(start);
        merge(start,result,0);
        return result;
    }
    
    void merge(vector<string>& vec,vector<vector<string>>& result,int start){
        for(int i = start; i<vec.size()-1;i++){
            
            if(i > 0){
                if(vec[i-1] == vec[i+1]){//以单个字符为中心进行回文合并
                    vector<string> vec_tmp = vec;//极大提高了运行效率,变量要在使用时再定义
                    vec_tmp[i-1] +=vec_tmp[i]+vec_tmp[i+1];
                    vec_tmp.erase(vec_tmp.begin()+i);
                    vec_tmp.erase(vec_tmp.begin()+i);
                    result.push_back(vec_tmp);
                    merge(vec_tmp,result,i-1);//合并后的一种分割方式路径
                }
            }
            
            if(vec[i] == vec[i+1]){//以两个相同字符为中心进行回文合并
                vector<string> vec_tmp2 = vec;
                vec_tmp2[i] += vec_tmp2[i+1];
                vec_tmp2.erase(vec_tmp2.begin()+i+1);
                result.push_back(vec_tmp2);
                merge(vec_tmp2,result,i);//合并后的一种分割方式路径
            }
        }
    }
};
  • 思路2:类似与单词分割II
  • 对于“aaabc”:

    • 如果“aaabc”是回文,那么单独构成一个解
    • 如果“aabc”是回文,那么递归求出“a”的结果,在结果的每个解后面push_back回文”aabc”,得到了一组解
    • 如果“abc”是回文,那么递归求出“aa”的结果,在结果的每个解后面push_back回文”abc”,得到了一组解
    • 如果“c”是回文,那么递归求出“aabc”的结果,在结果的每个解后面push_back回文”aabc”,得到了一组解
    • 在递归过程中,为了防止求重复子问题,使用mem保存状态,即每个子问题的解,如果后续需要求相同子问题的解,可以从mem中直接取出结果
class Solution {
public:
    vector<vector<string>> partition(string s) {
        unordered_map<int,vector<vector<string>>> mem;
        partition(s,s.length() - 1,mem);
        return mem[s.length() - 1];
    }
private:
    void partition(const string &s,int r,unordered_map<int,vector<vector<string>>> &mem){
        if(mem.find(r) != mem.end())    return;
        
        if(isPalindrome(s.substr(0,r + 1)))
            mem[r].push_back(vector<string>({s.substr(0,r + 1)}));
        
        for(int len = r;len >= 1;len--){//右边子串的长度
            int leftLen = r + 1 - len;
            const string &right = s.substr(leftLen,len);
            if(isPalindrome(right)){
                partition(s,leftLen - 1,mem);
                for(vector<string> left : mem[leftLen - 1]){
                    left.push_back(right);
                    mem[r].push_back(left);
                }
            }
        }
    }
    
    bool isPalindrome(const string &s){
        int i = 0,j = s.length() - 1;
        while(i < j && s[i] == s[j]){
            i++;
            j--;
        }
        return i >= j;
    }
};

回文分割的最小次数

给定一个字符串str,返回把str全部切割成回文字符串子串的最小分割数

输入:str = "ABA"//不需要切割
输出:0

输入:str = "ACDCDCDAD"
输出:2

思路:动态规划

  1. dp[size]记录str[0~i]的最小分割数
  2. p[i][j]记录str[i~j]是否是一个回文串
  3. 从str[0]开始从左到右开始遍历至str[j],然后逐个向前比较str[i] == str[j]
    • 如果相等,则判断它们中间的字符串str[i+1][j-1]是否为回文字符串,是,那么其中的一个可能的分割次数为dp[i-1]+1
    • 向前比较完成后,就可以得到dp[j]的值,即str[0~j]的最小分割次数
  4. 如此遍历到str的最后一个字符,返回dp[size]即可

代码实现:


class Solution {
public:
    int minCut(string str) {
        int *dp = new int[str.size()];
        bool **p = new bool*[str.size()];
        for (int i = 0; i < str.size(); i++) {
            dp[i] = INT_MAX;
            p[i] = new bool[str.size()];
        }

        //初始化 工作
        for (int i = 0; i < str.size(); i++) {
            p[i][i] = true;
        }

        dp[0] = 0;
        dp[1] = p[0][1] ? 0 : 1;
        for (int i = 0; i < str.size()-1; i++) {
            if(str[i] == str[i+1])
                p[i][i+1] = true;
            p[i][i + 1] = false;
        }

        //开始遍历
        for (int j = 2; j < str.size(); j++) {
            //向前逐个比较
            for (int i = j-2; i >= 0; i--) {
                if (str[i] == str[j]&&p[i+1][j-1]) {
                    if (i > 0)
                        dp[j] = min(dp[i - 1] + 1, dp[j]);//返回可能值的较小值
                    else
                        dp[j] = 0;
                    p[i][j] = true;
                }
                else {
                    p[i][j] = false;
                    dp[j] = min(dp[j - 1] + 1, dp[j]);
                }
            }
        }

        return dp[str.size() - 1];
    }
};

LeetCode200

岛屿的数量 (LeetCode200:Number of Islands)

给定一个二位数组,每一个数字非1即0,1代表陆地,0代表海水,当1的四周被海水包围时才形成一个岛屿,多个1可以连接成一个岛屿。我们假设二维数组的周边都是海水,问该二位数组中有多少个岛屿?

Input:
11110
11010
11000
00000

Output: 1 //所有的1连接成了一个岛屿

Input:
11000
11000
00100
00011

Output: 3

思路:使用深度优先遍历dfs,将所有连在一起的1全部找到,然后构成一个岛屿

class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        if(grid.empty()){
            return 0;
        }
        int col_size = grid.size();
        int raw_size = grid[0].size();
        //初始化访问数组
        int ** visited = new int *[col_size];
        for(int i = 0;i<col_size;i++){
            visited[i] = new int [raw_size];
        }
        
        for(int i = 0;i< col_size;i++){
            for (int j = 0;j<raw_size;j++){
                visited[i][j] = 0;
            }
        }
        
        int count = 0;
        for(int i = 0;i< col_size;i++){
            for (int j = 0;j<raw_size;j++){
                
                if(visited[i][j]== 0&&grid[i][j] == '1'){
                    numIslandx(grid,visited,i,j,col_size-1,raw_size-1);//深度优先遍历,将所有连在一起的1全部标记为已访问
                    count++;
                }
            }
        }
        return count;
    }
    //深度优先遍历
    void numIslandx(vector<vector<char>>& grid,int** visited,int col,int raw,int col_end,int raw_end){
        if(visited[col][raw] == 0 && grid[col][raw] == '1'){
            visited[col][raw] =1;//要先改变访问权限,否则会出现循环访问错误
            //四个方向
            if(col>0)
                numIslandx(grid,visited,col-1,raw,col_end,raw_end);
            if(raw>0)
                numIslandx(grid,visited,col,raw-1,col_end,raw_end);
            if(col<col_end)
                numIslandx(grid,visited,col+1,raw,col_end,raw_end);
            if(raw<raw_end)
                numIslandx(grid,visited,col,raw+1,col_end,raw_end);
        }
        return;
    }
};

LeetCode207

课程安排 (LeetCode207:Course Schedule)

有n门课需要学生学习,但其中有一些课之间存在先后顺序的依赖关系如(0,1)表示课程1需要在课程0之后学习,给出所有课程的依赖关系后,问学生能否按这种依赖关系学习完所有的课程。

Input: 2, [[1,0]] 
Output: true
Explanation: There are a total of 2 courses to take. 
             To take course 1 you should have finished course 0. So it is possible.

Input: 2, [[1,0],[0,1]]
Output: false
Explanation: There are a total of 2 courses to take. 
             To take course 1 you should have finished course 0, and to take course 0 you should
             also have finished course 1. So it is impossible.

我的思路:能否上完所有的课程取决于这些依赖关系是否构成回环

  1. 所以我们给所有的课程设置一个访问记录
  2. 先将所有的边按起点进行排序,从第一条边开始dfs深度优先遍历,遍历过程中设置课程的访问情况,回溯时需要消除对课程的访问记录
  3. 若在深度遍历过程中碰到一个曾经访问过的课程,说明出现回环。
  4. 并且每条边也有访问信息的记录,只要曾经访问过的边没有出现回环,之后也不用再进行深度优先遍历
  5. 如此将所有的边遍历一边后,都没有出现回环,则说明可以学习所有的课程

PS:

  1. 排序算法sort的使用
  2. 搜索函数equal_range的使用

代码实现:

class Solution {
public:
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
        int *visited = new int[numCourses];
        int *visited_edge = new int[prerequisites.size()];
        sort(prerequisites.begin(),prerequisites.end(),[](pair<int,int> p1,pair<int,int> p2){
            return p1.first<p2.first;
        });//现将所有的依赖关系边,按起点进行排序,因为,我们之后需要根据排序好的这些边,搜索相邻的边
        
        for (int i = 0;i<numCourses;i++){
            visited[i] = 0;
        }
        
        for (int i = 0;i<prerequisites.size();i++){
            visited_edge[i] = 0;
        }
        int end = prerequisites.size()-1;
        bool result = true;
        for(int i = 0;i<end;i++){//遍历所有的边
            if(visited_edge[i] == 0){//若该边已经被访问过了,则无需再次验证
                visited[prerequisites[i].first] = 1;//设置该边起点的访问记录
                result &= edge_dfs(visited,visited_edge,prerequisites,prerequisites[i].second);//开始进行深度优先遍历
                if(!result)
                    return false;
                visited[prerequisites[i].first] = 0;//消除该起点的访问记录
            }
        }
        
        return result;
    }
    
    bool edge_dfs(int *visited,int *visited_edge,vector<pair<int, int>>& prerequisites,int value){
        pair<int, int> valuepair(value,1);
        pair<vector<pair<int, int>>::iterator,vector<pair<int, int>>::iterator> range = equal_range(prerequisites.begin(),prerequisites.end(),valuepair,[](pair<int, int> p1,pair<int, int> p2){
            return p1.first<p2.first;
        });//根据排好序的边,搜索以起点value开始的所有边,也就是邻边
        bool result = true;
        int len = range.second - range.first;//计算邻边个数,为0,说明没有回环
        if(len == 0)
            return true;
        int newstart = range.first-prerequisites.begin();
        for(int i = newstart,j=0;i<newstart+len;i++,j++){//在以所有的邻边为起点开始深度优先遍历
            if(visited[(range.first+j)->first] == 1){//访问到重复节点,说明有环
                return false;
            }
            if(visited[(range.first+j)->first] ==1)
                return true;//访问到之前访问过的边,该边之前访问时无环,无需重复访问
            visited[(range.first+j)->first] = 1;
            visited_edge[i] = 1;//设置边的访问记录
            result = edge_dfs(visited,visited_edge,prerequisites,prerequisites[i].second);//开始深度优先遍历
            if(!result)
                return false;
            visited[(range.first+j)->first] = 0;//消除对点的访问记录
            
        }
        
        return true;
    }
};

思路2:使用DFS的拓扑排序,使用栈来实现 参考

1.计算所有点的入度 2.将入度为0的顶点入栈 3.出栈,获取顶点A,并将以A顶点为起始点的顶点B的入度减一,如果减一后,B的入度为0,则将B入栈 4.如果还没有将numCourses个顶点输出完,就已经空栈,则说明有环

class Solution {
public:
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) 
    {
        int* inDgree = new int[numCourses];
        memset(inDgree,0,sizeof(int)*numCourses);
        //记录每个顶点的入度
        for(auto p : prerequisites)
        {
            ++inDgree[p.first];
        }
        int top = -1;
        //将度为0的顶点入栈
        for(int i=0; i<numCourses; ++i)
        {
            if(!inDgree[i])
            {
                inDgree[i] = top;
                top = i;
            }
        }
        bool isOK = true;
        for(int i=0; i<numCourses; ++i)
        {
            //如果还没输出numCourses个点,栈就为空,说明有环
            if(top==-1) 
            {
                isOK = false;
                break;
            }
            //出栈
            int j = top; 
            top = inDgree[top];
            for(auto p : prerequisites)
            {
                if(p.second==j) 
                {
                    --inDgree[p.first];
                    if(!inDgree[p.first])
                    {
                        inDgree[p.first] = top;
                        top = p.first;
                    }
                }
                
            }  
        }
        delete[] inDgree;
        return isOK;
    }
};

LeetCode210

课程表II(LeetCode210:Course ScheduleII)

题目同LeetCode207,只不过这里需要输出课程的可能的安排顺序。

思路同LeetCode207,需要将栈中弹出的课程进行保存

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<pair<int, int>>& prerequisites) {
        int *inDegrees = new int[numCourses];
        stack<int> sta;
        //初始化所有的入度数组
        for(int i = 0;i<numCourses;i++){
            inDegrees[i] = 0;
        }
        //设置所有课程节点的入度
        for(auto pair:prerequisites){
            inDegrees[pair.first]++;
        }
        
        for(int i = 0;i<numCourses;i++){
            if(!inDegrees[i]){
                sta.push(i);
            }
        }
        vector<int> course_order;
        vector<int> empty_order;
        while(course_order.size()!=numCourses){
            if(sta.empty()){//栈为空
                delete[] inDegrees;
                return empty_order;
            }
            course_order.push_back(sta.top());
            int course_tmp = sta.top();
            sta.pop();//弹出后,对应元素的入度减少
            for(auto pair:prerequisites){
                if(pair.second == course_tmp){
                    inDegrees[pair.first]--;//入度减少
                    if(inDegrees[pair.first]==0)
                        sta.push(pair.first);//入度为0,压入栈
                }
            }
                
        }
        delete[] inDegrees;
        return course_order;
        
        
    }
};

LeetCode329

二维数组最长递增路径(LeetCode329:Longest Increasing Path in a Matrix)

给出一个二维数组,找出其中一条最长的递增序列的路径,返回其长度。

Input: nums = 
[
  [9*,9 ,4],
  [6*,6 ,8],
  [2*,1*,1]
] 
Output: 4 
Explanation: The longest increasing path is [1, 2, 6, 9].

Input: nums = 
[
  [3*,4*,5*],
  [3 ,2 ,6*],
  [2 ,2 ,1 ]
] 
Output: 4 
Explanation: The longest increasing path is [3, 4, 5, 6]. Moving diagonally is not allowed.

思路:使用递归加深度优先搜索,进一步可以使用动态规划优化递归过程。不需要使用访问记录表,因为在递归递增序列的过程当中不会对一个元素进行重复遍历。

代码实现:

class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if(matrix.empty())
            return 0;
        int col_size = matrix.size();
        int raw_size = matrix[0].size();
        int **record = new int*[col_size];
        for(int i = 0;i<col_size;i++){
            record[i] = new int[raw_size];
        }
        
        for(int i = 0;i<col_size;i++){
            for(int j = 0;j<raw_size;j++){
                record[i][j] = 0;//初始化记录以每个节点开始的最长递增序列的长度
            }
        }
        
        int maxlen = 0;
        for(int i = 0;i<col_size;i++){
            for(int j = 0;j<raw_size;j++){
                if(record[i][j] == 0){//若最长递增路径长度记录中没有改记录,则进行递归深度遍历
                    int len = dfs(matrix,record,i,j,col_size,raw_size);
                    maxlen = len>maxlen?len:maxlen;
                }
                
            }
        }
        
        return maxlen;
        
        
    }
    
    int dfs(vector<vector<int>>& matrix,int **record,int i,int j,int col_size,int raw_size){
        int maxlen = 1;
        //四个方向进行深度优先遍历
        if(i-1>=0 && matrix[i][j]<matrix[i-1][j]){
            int len = 0;
            if(record[i-1][j]==0){
                len = 1+dfs(matrix,record,i-1,j,col_size,raw_size);
                maxlen = len>maxlen?len:maxlen;
            }else{
                len = 1+record[i-1][j];
                maxlen = len>maxlen?len:maxlen;
            }       
        }
        
        if(j-1>=0 && matrix[i][j]<matrix[i][j-1]){
            int len = 0;
            if(record[i][j-1]==0){
                len = 1+dfs(matrix,record,i,j-1,col_size,raw_size);
                maxlen = len>maxlen?len:maxlen;
            }else{
                len = 1+record[i][j-1];
                maxlen = len>maxlen?len:maxlen;
            }       
        }
        
        if(i+1<col_size && matrix[i][j]<matrix[i+1][j]){
            int len = 0;
            if(record[i+1][j]==0){
                len = 1+dfs(matrix,record,i+1,j,col_size,raw_size);
                maxlen = len>maxlen?len:maxlen;
            }else{
                len = 1+record[i+1][j];
                maxlen = len>maxlen?len:maxlen;
            }       
        }
        
        if(j+1<raw_size && matrix[i][j]<matrix[i][j+1]){
            int len = 0;
            if(record[i][j+1]==0){
                len = 1+dfs(matrix,record,i,j+1,col_size,raw_size);
                maxlen = len>maxlen?len:maxlen;
            }else{
                len = 1+record[i][j+1];
                maxlen = len>maxlen?len:maxlen;
            }       
        }
        
        record[i][j] = maxlen;//记录到表格中
        return maxlen;
        
    }
};

LeetCode395

至少重复出现k次的最长子串(LeetCode395: Longest Substring with At Least K Repeating Characters)

给定一个字符串,查找该字符串中最长子串的长度,该子串满足所有的字符出现的次数均超过k次

Input:
s = "aaabb", k = 3

Output:
3

The longest substring is "aaa", as 'a' is repeated 3 times.

Input:
s = "ababbc", k = 2

Output:
5

The longest substring is "ababb", as 'a' is repeated 2 times and 'b' is repeated 3 times.

思路:

  1. 遍历一遍字符串,获取每个字符的出现次数
  2. 从左到右获取第一个少于k的字符,以此字符为中心,分隔成两个字符,递归调用本函数
  3. 如果不存在少于k的字符,说明本字符串是符合要求的
class Solution {
public:
    int longestSubstring(string s, int k) {
        vector<int> m(26,0);
        int sz = s.size();
        for(int i=0; i<sz; ++i) ++m[s[i]-'a'];
        int index = 0;
        while(index < sz && m[s[index]-'a']>=k) ++index;
        if(index==sz) return sz;
        
        //将[0,index)字符串递归处理
        int left = longestSubstring(s.substr(0,index), k);
        //跳过那些不满足k的字符
        while(index < sz && m[s[index]-'a']<k) ++index;
        //将[index,end)传入
        int right = longestSubstring(s.substr(index), k);
        
        return max(left,right);
    }
};

LeetCode39

找到数组中元素和为给定值的所有组合,不限元素使用次数

给定一个数组arr,输出arr中所有元素组合使得其和为指定target的组合。且arr中的元素可以重复使用

Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
  [7],
  [2,2,3]
]

Input: candidates = [2,3,5], target = 8,
A solution set is:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

思路:该题类似于“换钱方法数”,可以用动态规划求出组合方法数,然后回溯得到决策过程中的所有组合

思路2:直接使用递归,dfs

遍历数组中的每个元素,有两种选择:

  1. 取用该元素
  2. 不取用该元素,只要不取用的话以后都不能再取用

递归代码实现:

class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> v;
        dfs(candidates,res,target,v,0);//开始递归dfs
        return res;
    }
    
    void dfs(vector<int>& can,vector<vector<int>>& res,int target,vector<int>& v,int start){
        if(target == 0){
            res.push_back(v);
            return;
        }
        if(target<0 || start == can.size()){
            return;
        }
        v.push_back(can[start]);//取当前元素
        dfs(can,res,target-can[start],v,start);
        v.pop_back();
        //不取当前元素只有一个选择
        dfs(can,res,target,v,start+1);
        
        
    }
};

递归ending

位运算,数学边界问题

LeetCode7

整型数反转(LeetCode7:Reverse Integer)

将一个整型数的高位和低位进行反转,负数不变号,如果一个整型数反转后溢出后则返回0:

例子:

  • 123 =》321
  • -12 =》-21
  • -120 =》-21

注意点:

  • 整型数的范围为 [-2^31,2^31-1];所以可能出现-2^31在范围内,而它的绝对值却不在整型范围内
  • 要计算2^31次方:long upper = (2L«30);//L不能掉
class Solution {
public:
    int reverse(int x) {
        bool positive = true;
        long x2 = (long)x;
        long upper = (2L<<30)-1;
        //处理正负号
        if(x2<0){
            positive = false;
            upper = upper-1;
            x2 = labs(x2);//这里不能用abs,abs无法处理-2^31的绝对值,因为它处理整型数,转换过来的正数会溢出
        }
        if(x2 == 0)
            return 0;
        //处理末端为0的情况
        while(!(x2%10))
            x2 /= 10;
        vector<long> digit;
        while(x2>=10)
        {
            digit.push_back(x2%10);
            x2 = (x2-x2%10)/10;
        }
        digit.push_back(x2);
        long result = 0;
        for(int k = 0;k<digit.size();k++){
            result = result*10 + digit[k];
            if(result>upper){
                return 0;
            }
        }
        if(positive)
            return result;
        else
            return -result;
    }
};

LeetCode29

两个整型数做除法,不使用乘除运算符(LeetCode29:Divide Two Integers)

给两个整数相除,得到除数,但不能使用乘除运算符

要求:

  1. 除数和被除数都是32位的整型数
  2. 除数不能为0
  3. 整型数的范围为:[-2^31,2^31-1],当相除后的结果溢出时,返回2^31-1

思路:使用位运算,逐个将结果中的每一位(二进制位)数求出。将除数向左«逐个移位(将除数乘以2)与被除数进行比较。如15/2的计算过程:

  1. 2乘以2等于4,小于15
  2. 4乘以2等于8,小于15
  3. 8乘以2等于16,大于15
  4. 所以这里累计4(2*2),然后用15-8=7
  5. 2乘以2等于4,小于7
  6. 4乘以2等于8,大于7,这里累计2。且7-4 = 3。
  7. 2乘以2等于4,大于3,累计1。且3-2 =1
  8. 1小于2,循环结束。将所有累计相加得到4+2+1=7
  9. 所以最后相除的结果等于7
class Solution {
public:
    int divide(int dividend, int divisor) {
        //处理边界问题
        long int_max = (2L<<30)-1,int_min = -(2L<<30);
        if(dividend == int_min && divisor == -1){
            return int_max;
        }
        
        long tmp ,res = 0,res_tmp = 0;
        //判断正负号
        bool positive = true;
        if((dividend > 0 && divisor<0)||(dividend < 0 && divisor > 0)){
            positive = false;
        }
        long divd = labs((long)dividend);
        long divs = labs((long)divisor);
        
        
        while(divd>=divs){
            tmp = divs;
            res_tmp = 1;
            while(divd >= (tmp <<= 1))
            {
                res_tmp <<= 1;
            }
            divd -= tmp >> 1;//循环一次,被除数减去一次移位后的除数
            res += res_tmp;
        }
        if(positive)
            return res;
        else
            return -res;
    }
};

LeetCode50

求幂myPow()(LeetCode50: Pow(x, n))

题目要求:实现pow(x,n)

  • -100.0 < x < 100.0
  • n 是一个32位有符号整数。
Input: 2.00000, -2
Output: 0.25000
Explanation: 2-2 = 1/22 = 1/4 = 0.25

思路:扫描n的二进制上的为1的bit,来计算幂的结果,例如:n为139,则其二进制为10001011,那么x^n == x^(1+2+8+128) == (x^1) * (x^2) * (x^8) * (x^128)

PS:需要考虑的特殊情况

  1. x取值为0时,0的正数次幂是1,而负数次幂是没有意义的;判断x是否等于0不能直接用“==”。
  2. 对于n取值INT_MIN时,-n并不是INT_MAX,这时需要格外小心。
  3. 尽量使用移位运算来代替除法运算,加快算法执行的速度。

判断double变量是否为零是否正确的题目。判断double变量是否为零 不能像我们直观想象的那样double d;if( d == 0 );这种做法是极其错误的,因为double是双精度的,他表示本身就是有精度误差的,所以这样判断零不正确。应正确步骤应该是先 定义一个精度范围,当double小于该精度范围时就可以判定double变量是否为0了。代码如下:

#define MIN_VALUE 1e-8
#define IS_DOUBLE_ZERO(d)  (abs(d) < MIN_VALUE)

代码:

class Solution {
public:
    double myPow(double x, int n) {
        if(n<0){
            if(n == INT_MIN)
                return 1/(myPow(x,INT_MAX)*x);
            else
                return 1/myPow(x,-n);
        }
        double ans = 1;
        if(n>0){
            while(n!=0){
                if(n&1) //只在bit为1的位乘如到结果中去
                    ans *= x;
                x *=x;
                n >>=1;
            }
        }
        
        return ans;
    }
};

LeetCode69

求平方根(LeetCode69:Sqrt(x))

如题,给出一个数,求它的平方根,若平方根不为整数,则返回小于该平方根的最大的整数。

思路:二分法求值

注意特殊情况:

  1. 乘以2时可以用位运算替代
  2. 注意二分法求平方根的过程中,可能出现有的数的平方大于整型数的最大可能的值INT_MAX,所以应该用long 来保存临时结果。
class Solution {
public:
    int mySqrt(int x) {
        if (x==0)
            return 0;
        long pre = 1, back = 1;
        long tmp = 1;//用long保存临时结果
        while(tmp < x){
            pre = back;
            back <<= 1;//位运算替代乘以2
            tmp =back*back;
        }
        long mid;
        while(pre <= back){// 二分法求值
            mid = (pre+back)/2;
            tmp = mid*mid;
            if(tmp > x)
                back = mid - 1;
            else if(tmp < x )
                pre = mid + 1;
            else
                return mid;
        }
        if(pre*pre>x)
            return pre-1;
        else
            return pre;
    }
};

LeetCode172

n的阶乘尾部0的个数(LeetCode172:Factorial Trailing Zeroes)

给定一个数n,求它的阶乘结果尾部0的个数

Input: 3
Output: 0
Explanation: 3! = 6, no trailing zero.

Input: 5
Output: 1
Explanation: 5! = 120, one trailing zero.

思路:

  1. 只有当乘数中包含一对2和5时,尾部才可能增加一个0
  2. 在n递增的过程中,5的个数肯定少于2,所以我们需要求出1~n中包含的因数5的个数
  3. 注意如25包含两个5,125包含3个5

代码实现

class Solution {
public:
    int trailingZeroes(int n) {
         return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);//递归深一层,相当于先求有多少个5,再求有多少个25,。。。
        
    }
};

LeetCode179

将所有数字拼成一个最大数(LeetCode179:Largest Number)

给出一个整型数组,将这些数字按一定顺序排放,组成一个最大的数字

Input: [10,2]
Output: "210"

Input: [3,30,34,5,9]
Output: "9534330"

思路:见剑指offer面试题45:把数组排成最小的数

PS:

  1. lamada表达式的使用
  2. sort函数的使用
  3. 数字到字符串的转换

代码实现:

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        sort(nums.begin(),nums.end(),[](int nums1,int nums2){
            return to_string(nums1)+to_string(nums2)>to_string(nums2)+to_string(nums1);
        });//传入一个自定义比较函数
        if(nums[0] == 0) return "0";
        string result;
        for(int i= 0;i<nums.size();i++)
            result +=to_string(nums[i]);
        return result;
    }
};

LeetCode190

翻转bits(LeetCode190: Reverse Bits)

给出一个数,输出将其二进制bit翻转后的数字。

Input: 43261596
Output: 964176192
Explanation: 43261596 represented in binary as 00000010100101000001111010011100, 
             return 964176192 represented in binary as 00111001011110000010100101000000.

思路:

  1. 对n从低位到高位进行逐位检测
  2. 将检测结果放入result低位后向前移动一位
  3. 检测结束后,所有bit都完成了翻转
class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        int result = 0;
        for(int i = 0;i<32;i++){
            result <<= 1;//将所得到的结果向前移动存放
            result |= (n&1);//位检测
            n >>= 1;//进行下一位的检测
        }
        return result;
    }
};

LeetCode191

位为1的个数(LeetCode191:Number of 1 Bits)

检测一个数n的二进制位中为1的个数

Input: 11
Output: 3
Explanation: Integer 11 has binary representation 00000000000000000000000000001011 

Input: 128
Output: 1
Explanation: Integer 128 has binary representation 00000000000000000000000010000000

思路同上LeetCode190

代码:

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0 ;
        for(int i = 0;i<32;i++){
            if(n&1)
                count++;
            n >>=1;
        }
        return count;
    }
};

LeetCode202

快乐数(LeetCode202: Happy Number)

快乐数的定义如下:如下循环,最后循环的平方和结果为1即为快乐数

Input: 19
Output: true
Explanation: 
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

思路:

  1. 循环求解
  2. 同求商LeetCode166,求循环小数的思路一致,使用record记录
  3. 当出现循环时,说明不为快乐数

代码实现:

class Solution {
public:
    int calculate(int n)//求平方和
    {
        int res = 0;
        while(n)
        {
            int temp = n%10;
            res += temp*temp;
            n /= 10;
        }
        return res;
    }
    bool isHappy(int n) {
        unordered_set<int> res;//用于记录
        while(n!=1)
        {
            if(res.find(n)!=res.end()) return false;//出现重复数,说明不为快乐数
            res.insert(n);
            n = calculate(n);
        }
        return true;
    }
};

LeetCode204

质数计数(LeetCode204:Count Primes)

给出一个数n,计算小于n的正数中有多少个质数

Input: 10
Output: 4
Explanation: There are 4 prime numbers less than 10, they are 2, 3, 5, 7.

思路:

  1. 因为所有的数如果不是质数,肯定可以由比它小的数相乘得到。
  2. 所以我们由小(最小从2开始)到大遍历,每遍历到一个数时,将小于n的所有可能的倍数都标记为已访问
  3. 所以只要再遍历到已访问的数时,就知道他不是一个质数
  4. 当遍历到一个数没有访问时,说明它不能由比它小的数相乘得到,则是一个质数

代码实现:

class Solution {
public:
    int countPrimes(int n) {
        if(n<=2)
            return 0;
        int* record = new int[n];
        for(int i = 0;i<n;i++){
            record[i] = 0;//初始化访问数组
        }
        int count = 0;
        for(int i=2;i<n;i++){
            if(record[i] == 0){//没有访问说明为质数
                for(int j = i,k=2;j<n;j=i*k,k++){//遍历当前数的所有的倍数
                    record[j] = 1;
                }
                count++;
            }
        }
        return count;
    }
};

LeetCode227

基础计算器II(LeetCode227:Basic Caculator II)

给出一个字符串,其中涉及到+ - * /的基础运算,根据运算符的优先级计算其运算后的结果

Input: "3+2*2"
Output: 7

Input: " 3/2 "
Output: 1

Input: " 3+5 / 2 "
Output: 5

思路1:比较直接不建议使用

  1. 先将所有的* /计算得到结果
  2. 然后进行+ - 的运算
class Solution {
public:
    int calculate(string s) {
        vector<int> num;
        vector<char> op;
        string numstr;
        int restmp;
        //处理乘除
        for (int i = 0; i<s.size(); i++) {
            if(!isdigit(s[i])){
                if(s[i] == ' '){
                    continue;
                }else{
                    op.push_back(s[i]);
                    continue;
                }
            }
            numstr.clear();
            while (isdigit(s[i]) && i<s.size()) {
                numstr = numstr + s[i];
                i++;
            }//循环结束时,要么字符串遍历结束,要么指向下一个运算符
            i--;
            if (!num.empty() && !op.empty()) {
                if (op.back() == '*') {
                    restmp = stoi(numstr)*num.back();
                    num.pop_back();
                    op.pop_back();
                    num.push_back(restmp);
                }else if (op.back() == '/') {
                    restmp = num.back() / stoi(numstr);
                    num.pop_back();
                    op.pop_back();
                    num.push_back(restmp);
                }
                else {
                    num.push_back(stoi(numstr));
                }
            }
            else {
                num.push_back(stoi(numstr));
            }
        }
        //处理加减
        int result = num.front();
        for (int i = 0; i<num.size() - 1; i++) {
            if (op[i] == '+')
                result += num[i + 1];
            if (op[i] == '-')
                result -= num[i + 1];
        }

        return result;
    }
};

思路2:

  1. 先将中序表达式转换成后缀表达式
  2. 然后根据后缀表达式进行依次计算

中序表达式转换成后序表达式的过程:

  1. 遇到数字直接输出
  2. 遇到操作符先压入栈,若该操作符优先级高于栈顶的操作符,则先输出栈顶操作符,直到栈顶操作符的优先级低于或等于要压入的操作符
  3. 循环1,2将字符串遍历完后,再讲栈内的操作符依次弹出得到后缀表达式

思路3:

  1. 遇到* / 直接计算得到数字后入栈
  2. 遇到-,入栈负数
  3. 将栈内所有数相加

注意点:

  1. istringstream读取整型数»
  2. std::accumulate累加函数用法
int calculate(string s) {
          char op = '+';
          int value;

          std::istringstream iss(s);
          std::vector<int> res;

          while (iss >> value) {
              switch (op) {
                  case '+': res.push_back(value);  break;
                  case '-': res.push_back(-value); break;
                  case '*': res.back() *= value;   break;
                  case '/': res.back() /= value;   break;
                }
                iss >> op; // get next operator
              }

              return std::accumulate(std::begin(res), std::end(res), 0);
    }

代码实现参考

LeetCode268

缺失数字(LeetCode268: Missing Number)

给出一个长度为n数组,所有数组内所有元素的范围在0~n之间,且没有重复的数字,求哪个数字缺失了。

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

Input: [9,6,4,2,3,5,7,0,1]
Output: 8

思路1:哈希表

思路2:求和后相减

思路3:异或运算,两个相同的数进行异或运算等于0

leetcode268

代码实现:

思路2:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int sum = 0;
        for(int num:nums)
            sum+=num;
        return ((nums.size()+1)*nums.size()/2)-sum;
    }   
};

思路3:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int res = 0;
        int n = nums.size();
        for(int i=0; i<=n; ++i)
        {
            res ^= i;
            if(i!=n) res ^= nums[i];
        }
        return res;
    }
};

LeetCode326

判断是否为3的幂(LeetCode326:Power of Three)

给定一个整数,判断该整数是否为3的幂

Input: 27
Output: true

Input: 0
Output: false

Input: 9
Output: true

Input: 45
Output: false

要求:不使用循环和递归

思路:

  1. 先求整数范围INT_MAX内,3的幂可以达到的最大幂maxpower
  2. 所有能被maxpower整除的数即为3的幂

PS:使用power函数,和log函数,求3的幂可以到达的最大幂

换底公式:leetcode326

因此要求int的表示的最大的3的幂,设为maxPower = 3^exp,exp = log3(INT_MAX)

根据换底公式有: log3(INT_MAX) = log2(INT_MAX) / log2(3)

因此,maxPower = power(3, (int)log2(INT_MAX) / log2(3))

代码实现:

class Solution {
public:
    bool isPowerOfThree(int n) {
        if(n<=0) return false;
        int maxpower = pow(3,(int)(log(INT_MAX)/log(3)));//求3的幂在整型数范围内的最大数
        return maxpower%n?false:true;//判断是否可以整除
    }
};

LeetCode371

不使用+求两个数的和

题目如题,不使用+求两个数的和。

位运算:异或+两个数相与向左移一位得到进位的值。如此循环加直到进位的值为0即可返回

class Solution {
public:
    int getSum(int a, int b) {
        if(b==0)
            return a;
        return getSum(a^b,(a&b)<<1);
    }
};

code317

不使用额外变量交换两个整数的值

如题:不使用额外变量交换两个整数的值

思路

a = a^b;
b = a^b;//前两个式子等同于:b = a^b^b;//b^b=0,所以b= 原a
a = a^b;//此时b=原a,a = 原a^原b,所以该式子等价于a = 原a^原b^原a = 原b,所以a = 原b

code319

只用位运算不用算术运算实现整数的加减乘除运算

给定两个32位整数a,b可正可负,可为0.不能使用算术运算符,分别实现a和b的加减乘除运算

思路:

  • 加:
    • 先用&操作符计算a,b相加产生的进位,再将进位向左移一位c
    • 再将a,b进行异或操作,得到结果d
    • 将c,d同样进行加法操作。直到进位c为0
  • 减:用位运算实现减法运算,实现a-b只要实现a+(-b)即可
  • 乘法:ab=a(2^0)* b0+a*(2^1)*b1 + a*(2^2)* b2…
    • a按b中每个位置上的bit相乘,也就是进行相应的左移操作
    • 再将根据每一个bit左移后的结果相加即可
  • 除法:乘法的逆运算

code329

在其他数都出现k次的数组中找到只出现一次的数

给定一个整型数组arr和一个大于1的整数k。已知arr中只有1个数出现了一次,其他的数都出现了k次,请返回只出现1次的数。

要求:时间复杂度为O(N),空间复杂度为O(1)

思路:

  1. 首先我们要知道,k个相同的k进制数无进位相加,结果一定是每一位上都是0的k进制数
  2. 所以我们首先将arr所有的数转换为k进制数,然后逐个进行无进位相加
  3. 最后得到的k进制转换为10进制即为只出现一次的数,因为所有出现k次的数都会变为0

位运算,数学,边界问题ending

字符串

LeetCode49

字符串分类(LeetCode49:Group Anagrams)

给一个数组,将所有字符相同的字符串分到一个数组里面去

Input: ["eat", "tea", "tan", "ate", "nat", "bat"],
Output:
[
  ["ate","eat","tea"],
  ["nat","tan"],
  ["bat"]
]

思路:利用unordered_map(哈希表)索引的性质,先将所有字符串进行排序,排序后一样的字符串放到一个数组。

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> result;
        if(strs.empty()){
            return result;
        }
        unordered_map<string,vector<string>> diff;
        for(int i= 0 ;i<strs.size();i++){
            string sorted_str = strs[i];
            sort(sorted_str.begin(),sorted_str.end());//对字符串进行排序
            if(diff.count(sorted_str)){//查看当前map中有没有出现过该索引
                diff[sorted_str].push_back(strs[i]);
            }else{
                vector<string> new_vec;
                new_vec.push_back(strs[i]);
                diff.insert(make_pair(sorted_str,new_vec));
            }
        }
        
        for(auto pair : diff){//map遍历的方法
            result.push_back(pair.second);
        }
        
        return result;
    }
};

ReverseString

判断两个字符串是否互为旋转词

如果一个字符串str,把字符串str前面的任意部分挪到后面形成的字符串叫做str的旋转字符串。

输入:a = "cdab" b = "abcd"  
输出:true

思路:

  1. 将两个字符串b拼在一起s = “abcdabcd”
  2. 可以发现该拼接字符串中任意长度为4的字符串均为字符串a(”cdab”)的旋转字符串
  3. 且s中必包含子串a(”cdab”),所以我们只要判断s中包含子串a即可
  4. 一个字符串包含子串的算法,使用KMP算法O(N)即可

KMP

KMP求解字符串的子串匹配

给出两个字符串src,match。求得src中匹配子串match的下标

传统思路:先匹配首字符,然后逐个匹配。如此循环遍历比较src的每个字符。当match完全匹配时则返回

KMP算法:(针对匹配字符串有重复字符的优势更大)

  • 当遍历到src中下标为i的字符时,匹配match的长度为j当src中第i+j下标的元素并不和match[j]匹配
  • 这时并不是跳到src[i+1]又开始重新匹配
  • 而是在src[i+j]的位置上,先找到该位置前面和match前缀匹配的所有可能的匹配长度new_j = nextarr[j](由长到短),继续下一个字符src[i+j]==match[new_j]?的匹配
  • 由nextarr数组当前match中i下标所在位置的元素前面以i-1结尾的字符串和match的前缀字符串最长匹配的长度。
  • 注意:当当前最长前缀匹配的字符match[new_j] != src[i+j]的下一个字符串也不匹配时,先前追溯第二长的前缀匹配:new_j = nextarr[new_j]。继续匹配。
  • 如果前面没有字符可以和match前缀部分匹配,也就是new_j==-1。那么src[i+j]匹配失败,直接递进到下一个元素src[i+j+1],且此时j==0,进行匹配

实例: 当在src字符串AWVABCABCABD,匹配ABCABD 当从src第4位匹配到第9位时(ABCAB),发现C不等于D,此时并不是从src第5位开始从头匹配,而是继续匹配src第9位,匹配长度从5变为2(需提前构建match的nextarr数组),此时匹配成功ABCABD

主要难点:

  1. nextarr数组的构建:match每个字符X在该数组保存了前一跳的下标i,使得首字符到i的字符串可以匹配,X字符的前缀字符串。
  2. 当当前长度的前缀不匹配时,向前跳找到第二长度的前缀进行匹配

代码实现:

class Solution {
public:
    int getIndexof(const char *str,const char *match) {
        int *next = getnextarr(match);
        int i = 0,j = 0;
        int slen = strlen(str);
        int mlen = strlen(match);
        while (i<slen && j<mlen) {
            if (str[i] == match[j]) {//匹配,增加匹配字符串下标进行下一轮匹配
                i++;
                j++;
            }//不匹配,找到当前j的最长前缀后缀匹配长度后进行下一轮匹配
            else if(next[j]<0){//说明没有匹配的元素,需要重头开始匹配
                i++;
            }
            else {
                j = next[j];//找到除最长前缀匹配的的其它前缀是否可以匹配
            }
        }
        return j == mlen?i-j:-1 ;
    }

    int* getnextarr(const char *match) {
        int len = strlen(match);
        int *nextarr = new int[len];
        nextarr[0] = -1;
        nextarr[1] = 0;
        int pos = 2;//pos代表要填充的nextarr的下标
        int cn = 0;//当前要进行比较的字符下标

        while (pos <= len) {
            if (match[pos - 1] == match[cn]) {
                //匹配成功,最长匹配长度加1即可,下一轮继续比较cn后一个字符
                nextarr[pos++] = ++cn;
            }
            else if(cn > 0){//向前跳,因为当前最长前缀后缀匹配字符串不能继续延续,不能保证比最长字符串小的匹配字符串不能继续匹配
                cn = nextarr[cn];
            }
            else {
                nextarr[pos++] = 0;
            }
        }
        return nextarr;
    }
};

MinDistanceBetweenStrings

数组中两个字符串的最小距离

给定一个字符串数组strs,再给定两个字符串str1和str2,返回在strs中str1和str2的最小距离,如果str1和str2为null,或不在strs中,则返回-1.

输入:strs = ["1","3","3","3","2","3","1"],str1 = "1",str2 = "2"

输出:2

思路:

  1. 遍历数组strs
  2. 一个指针last1,记录字符串str1最后出现的位置,一个指针last2,记录字符串str2最后出现的位置
  3. 没碰到一个str1或str2时,则根据last1,last2更新一次最短距离min
  4. 遍历结束后即可以得到最短距离

进阶:如何实现查询最短距离的复杂度为O(1)

  • 构建一个map < key, map< key,min»结构,索引为str1,值依然为一个map结构,其key依然为另一个str2。
  • 该map一个key :str1记录它与其它任何其它str2的最短距离
  • 所以当我们查询任意两个str之间的最短距离直接访问该map结构即可
  • 生成该记录map的时间复杂度为O(N^2),空间复杂度为O(N^2),但之后的查询时间复杂度为O(1)

code276

公式字符串求值

给定一个字符串str,str表示一个公式,公式里面可能有整数,加减乘除符号和左右括号,返回公式计算结果。

输入:str = "48*((70-65)-43)+8*1"

输出: -1816

输入:str = "3-1*4"

输出:7

思路:使用递归,遇到一个括号‘(’就进行递归,遇到’)’进行递归返回。递归求得一个()内表达式的值即可

code278

0左边必有1的二进制字符串的数量

给定一个整数N,求由“0”字符和“1”字符组成的长度为N的所有字符串中,满足“0”的字符左边必有一个“1”字符条件的字符串数量

N=1 :  "0","1",只有字符串"1"满足,返回1
N=2:  "01","10","00","11"。字符串"10","11"满足,返回2

思路:递归,遍历该字符串,碰到每一位,分别取0和取1进行递归

  • 取0时,下一位只能为1。
  • 取1时,下一位任意。

思路2:我们可以根据规律:

  • N=1 时 count = 1
  • N=2 时 count = 2
  • N=3 时 count = 3
  • N=4 时 count = 5
  • N=5 时 count = 8
  • 。。。
  • 说明该数列满足斐波拉契数列的规律
  • 所以我们可以直接根据其规律进行计算,进一步优化可以用矩阵相乘的办法来求斐波拉契数列降低复杂度

code284

找到字符串的最长无重复字符子串

给定一个字符串str,返回str的最长无重复字符子串的长度

str = "abcd"  返回4

思路O(N):(类似于动态规划,只不过使用空间压缩,并且通过map不用进行第二次遍历)

  • 遍历一边字符串即可,用map记录所有字符最近出现的位置下标
  • 当遍历str[i]时,pre记录以str[i-1]结尾的最长无重复字符串的开始位置的前一个位置下标
  • 首先通过map查询str[i]之前最近出现的下标a
    • 若a在pre之前,则以str[i]结尾的最长无重复字符串只能以pre的下一个位置开始
    • 若a在pre之后,则以str[i]结尾的最长无重复字符串只能以a的下一个位置开始
  • 然后更新pre,map[str[i]],maxlen,遍历下一个字符
  • 遍历结束后即可得到最长的无重复字符串的长度maxlen

code299

字典树的实现

字典树又称为前缀树,或Trie树,是处理字符串常见的数据结构。假设组成的所有单词仅是”a”~”z”,请实现字典树结构,并包含以下四个主要功能。

  • void insert(string word):添加word,可重复添加
  • void delete(string word):删除word,如果word添加过多次,仅删除一个
  • boolean search(string word):查询word是否在字典树中
  • int prefixNumber(string pre):返回以字符串pre为前缀的单词数量

  • 字典树的介绍:字典树是一种树形结构,优点是利用字符串的公共前缀来节约存储空间,比如加入”abc”,”abcd”,”abd”,”b”,”bcd”,”efg”,”hik”

wordTree

字典树节点定义:

public class TrieNode{
public: 
    int path;//代表有多少单词共用该节点
    int end;//代表有多少个单词以这个节点结尾
    TrieNode[] map;//长度为26的数组代表26个字母不同的路径
    public TrieNode():path(0),end(0){
        map = new TrieNode[26];
    }
}

字符串ending

LeetCode94

二叉树的中序遍历(LeetCode94:Binary Tree Inorder Traversal)

给一个二叉树,输出它的中序遍历序列,不使用递归,而是迭代的方法来求其序列:

Input: [1,null,2,3]
   1
    \
     2
    /
   3

Output: [1,3,2]

思路:递归都可以使用栈来代替:

  1. 先向左遍历,将沿路径的所有节点压入栈结构,遇到空节点时弹出栈顶元素,输出到序列
  2. 检测其右节点是否为空,不为空,重复步骤1
  3. 如此循环迭代,直到栈为空。
/**
 * 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 {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> stack;
        TreeNode *cur = root;
        
        while(true){
            if(cur!=NULL){
                stack.push(cur);
                cur = cur->left;//向左遍历,沿路所有节点压入栈
            }else{
                if(stack.empty()){//栈为空时遍历结束
                    break;
                }
                cur = stack.top();
                result.push_back(cur->val);
                stack.pop();
                if(cur->right!=NULL){//检测右节点是否为空
                    cur = cur->right;
                }else{
                    cur = NULL;
                }  
            } 
        }
        return result;
    }
};

LeetCode98

验证是否是二叉搜索树(LeetCode98:Validate Binary Search Tree)

如题,思路:中序遍历是否为递增序列,借鉴题94的求中序遍历的做法。

注意:

  1. NULL为0,0==NULL
  2. INT_MIN = -2147483648
/**
 * 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 {
public:
    bool isValidBST(TreeNode* root) {
        long curnum = -2147483649;
        stack<TreeNode*> stack;
        TreeNode *cur = root;
        bool result = true;
        
        while(true){
            if(cur!=NULL){
                stack.push(cur);
                cur = cur->left;//向左遍历,沿路所有节点压入栈
            }else{
                if(stack.empty()){//栈为空时遍历结束
                    break;
                }
                cur = stack.top();
                if(cur->val > curnum){//比较,是否是递增序列
                    curnum = cur->val;
                }else{
                    result = false;
                    break;
                }
                stack.pop();
                if(cur->right!=NULL){//检测右节点是否为空
                    cur = cur->right;
                }else{
                    cur = NULL;
                }  
            } 
        }
        return result;
    }
};


LeetCode101

验证是否是对称树(LeetCode101:Symmetric Tree)

给出一个树的结构,判断它是否是对称树:

True:

    1
   / \
  2   2
 / \ / \
3  4 4  3

False:

    1
   / \
  2   2
   \   \
   3    3

思路:按层遍历,使用栈作为辅助结构,一个从左到右遍历,一个从右到左遍历,在弹出的同时比较节点值是否相等。不能忽略空节点

/**
 * 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 {
public:
    bool isSymmetric(TreeNode* root) {
        stack<TreeNode *> sta;
        stack<TreeNode *> sta_sym;
        TreeNode * cur = nullptr,* cur_sym = nullptr;
        sta.push(root);
        sta_sym.push(root);
        bool result =false;
        while(true){
            //只有当两个栈同时为空,才算对称树
            if(sta.empty() && sta_sym.empty()){
                result = true;
                break;
            }else if(sta.empty() || sta_sym.empty()){
                break;
            }
            cur = sta.top();
            cur_sym = sta_sym.top();
            //比较栈顶元素
            if(cur == nullptr && cur_sym == nullptr){
                sta.pop();
                sta_sym.pop();
            }else if((cur != nullptr && cur_sym != nullptr)&&(cur->val == cur_sym->val)){
                sta.pop();
                sta_sym.pop();
                //从左到右顺序,层次遍历入栈
                if(cur->left != NULL || cur->right != NULL){
                if(cur->left == NULL)
                    sta.push(nullptr);
                else
                    sta.push(cur->left);
                
                if(cur->right == NULL)
                    sta.push(nullptr);
                else
                    sta.push(cur->right);
                }
                
                //从右到左的顺序,层次遍历入栈
                if(cur_sym->left != NULL || cur_sym->right != NULL){
                if(cur_sym->right == NULL)
                    sta_sym.push(nullptr);//空指针也要入栈
                else
                    sta_sym.push(cur_sym->right);
                
                if(cur_sym->left == NULL)
                    sta_sym.push(nullptr);
                else
                    sta_sym.push(cur_sym->left);
                }
            }else{
                break;
            }
            
        }
        
        return result;
    }
};

LeetCode102

层次遍历打印(LeetCode102:Binary Tree Level Order Traversal)

描述:给一棵树,层次遍历打印其元素

    3
   / \
  9  20
    /  \
   15   7

[
  [3],
  [9,20],
  [15,7]
]

思路:使用队列,记录每一层最后一个元素last

/**
 * 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 {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        if(root == NULL){
            return result;
        }
        TreeNode *last = root,*cur;
        queue<TreeNode*> que;
        vector<int> cur_vec;
        que.push(root);
        
        
        while(!que.empty()){
            cur  = que.front();
            cur_vec.push_back(cur->val);
            
            if(cur == last){//打印到该层最后一个节点时
                result.push_back(cur_vec);
                cur_vec.clear();
                que.pop();
                if(cur->left != NULL)
                    que.push(cur->left);
                if(cur->right != NULL)
                    que.push(cur->right);
                last = que.back();
            }else{
                que.pop();
                if(cur->left != NULL)
                    que.push(cur->left);
                if(cur->right != NULL)
                    que.push(cur->right);
            }
        }
        
        return result;
    }
};

LeetCode103

Zigzag打印(LeetCode103:Binary Tree Zigzag Level Order Traversal)

之字型打印,和Leetcode102略有区别。

    3
   / \
  9  20
    /  \
   15   7

[
  [3],
  [20,9],
  [15,7]
]

思路:使用双端队列,打印一层后换向打印,使用bool变量seq来控制打印方向。

/**
 * 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 {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> result;
        if(root == NULL){
            return result;
        }
        TreeNode *last = root,*cur;
        deque<TreeNode*> que;
        vector<int> cur_vec;
        que.push_back(root);
        bool seq = true;
        
        
        while(!que.empty()){
            if(seq){//seq控制打印方向
                cur  = que.front();
                cur_vec.push_back(cur->val);
                if(cur == last){//打印到该层最后一个节点时
                    result.push_back(cur_vec);
                    cur_vec.clear();
                    que.pop_front();
                    if(cur->left != NULL)
                        que.push_back(cur->left);
                    if(cur->right != NULL)
                        que.push_back(cur->right);
                    last = que.front();
                    seq = !seq;
                }else{
                    que.pop_front();
                    if(cur->left != NULL)
                        que.push_back(cur->left);
                    if(cur->right != NULL)
                        que.push_back(cur->right);
                }
            }else{//逆序打印
                cur  = que.back();
                cur_vec.push_back(cur->val);
                if(cur == last){//打印到该层最后一个节点时
                    result.push_back(cur_vec);
                    cur_vec.clear();
                    que.pop_back();
                    if(cur->right != NULL)
                        que.push_front(cur->right);
                    if(cur->left != NULL)
                        que.push_front(cur->left);
                    last = que.back();
                    seq = !seq;
                }else{
                    que.pop_back();
                    if(cur->right != NULL)
                        que.push_front(cur->right);
                    if(cur->left != NULL)
                        que.push_front(cur->left);
                }
            }
        }
        
        return result;
    }
};

LeetCode104

求二叉树的高度(LeetCode104:Maximum Depth of Binary Tree)

给出一棵二叉树,求其高度

思路:

  1. 深度优先,递归求解
  2. 广度优先,利用队列,按层遍历,求层的数目即可

  3. 深度优先,两种解法
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(root == NULL)
            return 0;
        int r_height = 0,l_height = 0;
        int height = 0;
        if(root->right!=NULL){
            r_height = maxDepth(root->right);
        }
        if(root->left!=NULL){
            l_height = maxDepth(root->left);
        }
        height = r_height >= l_height?r_height+1:l_height+1;
        return height;
        
    }
};



//一行代码即可

int maxDepth(TreeNode *root)
{
    return root == NULL ? 0 : max(maxDepth(root -> left), maxDepth(root -> right)) + 1;
}
  1. 广度优先:

int maxDepth(TreeNode *root)
{
    if(root == NULL)
        return 0;
    
    int res = 0;
    queue<TreeNode *> q;
    q.push(root);
    while(!q.empty())
    {
        ++ res;
        for(int i = 0, n = q.size(); i < n; ++ i)
        {
            TreeNode *p = q.front();
            q.pop();
            
            if(p -> left != NULL)
                q.push(p -> left);
            if(p -> right != NULL)
                q.push(p -> right);
        }
    }
    
    return res;
}

LeetCode105

根据前序和中序遍历序列构造树结构(LeetCode105: Construct Binary Tree from Preorder and Inorder Traversal)

思路:递归求解,根据前序遍历第一个元素为根节点的特点,再在中序遍历中寻找这个元素,根据该元素将中序遍历和前序遍历的序列划分为两个序列。然后依次递归求解

难点:处理序列划分时的边界问题


/**
 * 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 {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(preorder.empty()){
            return NULL;
        }
        TreeNode* root = buildTree_x(preorder,inorder,0,preorder.size()-1,0,inorder.size()-1);
        return root;
    }
    
    TreeNode* buildTree_x(vector<int>& preorder, vector<int>& inorder,int pstart,int pend,int istart,int iend){
        TreeNode * cur = new TreeNode(preorder[pstart]);
        if(pstart == pend){
            return cur;
        }
        int new_pstart1 = pstart + 1,new_pend1,new_pstart2,new_pend2 = pend;
        int new_istart1 = istart,new_iend1,new_istart2,new_iend2 = iend;
        
        auto iter = find(inorder.begin(),inorder.end(),preorder[pstart]);
        int len = iter-inorder.begin()-istart;
        
        new_pend1 = pstart + len;
        new_pstart2 = new_pend1+1;
        
        new_iend1 = new_istart1+len-1;
        new_istart2 = new_iend1+2;
        //左右子树构成的新序列的边界确定
        
        if(len>0){
            cur->left = buildTree_x(preorder,inorder,new_pstart1,new_pend1,new_istart1,new_iend1);
        }
        
        if(new_pend1 < pend){
            cur->right = buildTree_x(preorder,inorder,new_pstart2,new_pend2,new_istart2,new_iend2);
        }
        
        return cur;
    }
};

LeetCode108

根据递增序列构造二叉查找树(LeetCode108:Convert Sorted Array to Binary Search Tree)

给一个递增序列,根据该序列构造一棵二叉查找树

Given the sorted array: [-10,-3,0,5,9],

One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST:

      0
     / \
   -3   9
   /   /
 -10  5

思路:递归不断寻找中点作为根节点,构造该树。


/**
 * 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 {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        if(nums.empty())
            return nullptr;
        TreeNode * root = sortedArrayToBST_x(nums,0,nums.size()-1);
        return root;
    }
    
    TreeNode* sortedArrayToBST_x(vector<int>& nums,int start, int end) {
        int mid = (start+end)/2;
        TreeNode* cur = new TreeNode(nums[mid]);
        if(mid > start)
            cur->left = sortedArrayToBST_x(nums,start, mid-1);
        if(mid < end)
            cur->right = sortedArrayToBST_x(nums,mid+1, end);
        return cur;
    }
};

LeetCode116

将树每一层用链表连接(LeetCode116:Populating Next Right Pointers in Each Node)

将树的每一层从左到右由链表连接,最后一个节点指向NULL

struct TreeLinkNode {
  TreeLinkNode *left;
  TreeLinkNode *right;
  TreeLinkNode *next;
}

     1
   /  \
  2    3
 / \  / \
4  5  6  7

连接为:

    1 -> NULL
   /  \
  2 -> 3 -> NULL
 / \  / \
4->5->6->7 -> NULL


思路:层次遍历,记录最后一个节点last,然后每弹出一个节点,设置其NEXT指针。


/**
 * 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 {
public:
    void connect(TreeLinkNode *root) {
        if(root == NULL){
            return ;
        }
        TreeLinkNode *last = root,*cur;
        queue<TreeLinkNode*> que;
        que.push(root);
        
        
        while(!que.empty()){
            cur  = que.front();
            
            if(cur == last){//打印到该层最后一个节点时
                que.pop();
                cur->next = NULL;
                if(cur->left != NULL)
                    que.push(cur->left);
                if(cur->right != NULL)
                    que.push(cur->right);
                last = que.back();
            }else{
                que.pop();
                cur->next = que.front();
                if(cur->left != NULL)
                    que.push(cur->left);
                if(cur->right != NULL)
                    que.push(cur->right);
            }
        }
        
        return ;
    }
};

LeetCode124

求树中最大路径和(LeetCode124:Binary Tree Maximum Path Sum)

给出一棵树结构,求出该树结构中,使得节点值总和最大的路径。返回该最大值总和,至少要有一个节点,不必经过根节点。


Input: [1,2,3]

       1
      / \
     2   3

Output: 6

Input: [-10,9,20,null,null,15,7]

   -10
   / \
  9 *20*
    /  \
  *15* *7*

Output: 42

思路:递归,用一个record记录节点的左右子树经过根节点的路径的最大总和。

  1. 先求左子树的最大路径和,和到左子树根节点路径的最大路径和
  2. 再求右子树的最大路径和,和到右子树根节点路径的最大路径和
  3. 比较左子树和右子树的最大路径和,及左子树到根节点的最大路径+右子树到根节点的最大路径+当前节点值。去其最大值,作为当前节点为根节点的子树的最大路径和。

/**
 * 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 {
public:
    int maxPathSum(TreeNode* root) {
        int* record = new int();
        int result = maxPathSum_x(root,record);
        delete record;
        return result;
    }
    
    int maxPathSum_x(TreeNode* cur,int* record){
        if(cur == NULL){
            *record = 0;
            return INT_MIN ;
        }
        
        int lMax = maxPathSum_x(cur->left,record);//左子树最大路径和
        int maxfromleft = *record;//到左根节点的最大路径和
        if(maxfromleft<0)
            maxfromleft = 0;
        int rMax = maxPathSum_x(cur->right,record);//右子树最大路径和
        int maxfromright = *record;//到右根节点的最大路径和
        if(maxfromright<0)
            maxfromright = 0;
        
        *record = max(maxfromleft,maxfromright)+cur->val;
        return max(max(lMax,rMax),maxfromleft+maxfromright+cur->val);//返回该树的最大路径和

    }
};

LeetCode230

求树中第k大的元素(LeetCode230:Kth Smallest Element in a BST)

给出一个二叉搜索树,求出该树中第k大的元素。

Input: root = [3,1,4,null,2], k = 1
   3
  / \
 1   4
  \
   2
Output: 1


Input: root = [5,3,6,2,4,null,null,1], k = 3
       5
      / \
     3   6
    / \
   2   4
  /
 1
Output: 3

思路:中序遍历的过程中,遍历到第k个元素时,返回即可

/**
 * 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 {
public:
    int kthSmallest(TreeNode* root, int k) {
        stack<TreeNode *> sta;
        TreeNode * cur = root;
        while(true){//迭代进行中序遍历
            if(cur){
                sta.push(cur);
                cur = cur->left;
            }
            else{
                k--;
                if(k==0)//当遍历到第k个元素时返回
                    return sta.top()->val;
                cur = sta.top()->right;
                sta.pop();
            }
            
        }
        
        return 0;
    }
    
};

LeetCode236

求两个节点的第一个公共祖先(LeetCode236:Lowest Common Ancestor of a Binary Tree)

给出一棵树,及其树上的两个节点,求这两个节点的第一个公共祖先

        _______3______
       /              \
    ___5__          ___1__
   /      \        /      \
   6      _2       0       8
         /  \
         7   4

Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
Output: 3
Explanation: The LCA of of nodes 5 and 1 is 3.


Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
Output: 5
Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself
             according to the LCA definition.

思路:

  1. 采用一次后序遍历的方式(迭代遍历),记录从根节点到两个节点的路径
  2. 从根节点开始比较两条路径,路径上最后一个相同的节点即为第一个公共祖先节点
/**
 * 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 {
public:
    TreeNode * lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        vector<TreeNode*> path_p;
        vector<TreeNode*> path_q;
        bool pfound = false, qfound = false;

        TreeNode*cur = root;
        TreeNode*pre_popp = NULL,*pre_popq = NULL;
        //中序遍历
        while (!pfound || !qfound) {//一次后序遍历,直到两个节点都被找到
            if (cur) {
                if (cur->val == p->val){
                    path_p.push_back(cur);
                    pfound = true;//找到p节点,做相应标记
                }
                    
                if (cur->val == q->val){
                    path_q.push_back(cur);
                    qfound = true;//找到q节点,做相应标记
                }
                    
                if (!pfound)
                    path_p.push_back(cur);
                if (!qfound)
                    path_q.push_back(cur);
                cur = cur->left;//先向左遍历
            }
            else {
                if (!pfound) {
                    if (path_p.back()->right&&path_p.back()->right != pre_popp)//同下
                     {
                        cur = path_p.back()->right;
                    }
                    else {
                        pre_popp = path_p.back();
                        path_p.pop_back();
                    }
                }

                if (!qfound) {
                    if (path_q.back()->right&&path_q.back()->right!= pre_popq) //若当前节点为空,且前一个被弹出的节点不是该节点的右节点,则获取其右节点。相当于后序遍历的第二步
                    {
                        cur = path_q.back()->right;
                    }
                    else {
                        //到这里,要么是没有右节点,要么是从右子树遍历完后回到该节点,则继续回溯寻找,且弹出该节点,因为该节点不在路径上
                        pre_popq = path_q.back();
                        path_q.pop_back();
                    }
                }
            }
        }

        TreeNode *first_root = root;
        int size = path_p.size()<path_q.size()?path_p.size():path_q.size();
        for (int i = 1; i<size; i++) {
            //确定从根节点到两个节点的路径后,然后从根节点开始比较,找到最后一个相同的节点为第一个公共祖先节点
            if (path_q[i] != path_p[i]) {
                break;
            }
            first_root = path_q[i];
        }

        return first_root;

    }
};

LeetCode297

二叉树的序列化和反序列化(LeetCode297:Serialize and Deserialize Binary Tree)

思路:见剑指offer

我的实现:使用栈代替了递归

class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        string result = "[";
        TreeNode *cur = root;
        stack<TreeNode*> sta;
        if (root != NULL) {
            result = result + to_string(root->val);
            sta.push(root);
        }else{
            result = result + "]";
            return result;
        }
        cur = cur->left;
        while (true) {//利用栈进行递归处理
            if (cur) {
                result = result + ",";
                result = result + to_string(cur->val);
                sta.push(cur);
                cur = cur->left;
            }
            else {
                result = result + ",";
                result = result + "#";
                if (sta.empty()) {
                    break;
                }
                cur = sta.top()->right;
                sta.pop();
            }
        }

        result = result + "]";
        return result;
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        if (data.empty()||data.size()==2) {
            return NULL;
        }
        int *len = new int();
        int start = 1;
        stack<TreeNode*> sta;
        string val = getval(data, start, len);
        TreeNode* root = new TreeNode(stoi(val));
        TreeNode** cur = &(root->left);
        sta.push(root);
        start += *len+1;
        val = getval(data, start, len);
        while (!val.empty()) {//利用栈进行递归处理
            if (val != "#") {
                *cur = new TreeNode(stoi(val));
                sta.push(*cur);
                cur = &((*cur)->left);
            }
            else {
                if (!sta.empty()) {
                    cur = &((sta.top())->right);
                    sta.pop();
                }
                else {
                    break;
                }
            }
            start += *len+1;
            val = getval(data, start, len);
        }

        return root;
    }
    string getval(string data, int index, int *len) {//获取下一个树节点的值
        string res;
        if (index == data.size() - 1) {
            return "";
        }
        int start = index;
        while ((data[index]>='0'&&data[index]<='9')|| data[index] == '-') {
            res = res + data[index];
            index++;
        }

        if (data[index] == '#') {
            res = res + data[index];
            index++;
        }

        *len = index - start;
        return res;
    }
};

思路参考

  1. 需要使用到ostringstream和istringstream
  • 直接使用字符串传递
  • 转为二进制再转为字符串传递
    • 优势:
      • 当节点值都非常大时,可以更节省空间
      • 不需要字符串到整型的转换
    • 缺点:
      • 不同系统(32位或64位)的整型的大小不一致
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        ostringstream out;
        serialize(root,out);
        return out.str();
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        istringstream in(data);
        return deserialize(in);
    }
private:
    void serialize(TreeNode* root, ostringstream& out)
    {
        if(!root)
        {
            out << "# ";
            return;
        }
        out << root->val << " ";//不需要字符串到int的转换
        serialize(root->left, out);
        serialize(root->right, out);
    }
    TreeNode* deserialize(istringstream& in)
    {
        string val;
        in >> val;
        if(val=="#") return nullptr;
        TreeNode* root = new TreeNode(stoi(val));
        root->left = deserialize(in);
        root->right = deserialize(in);
        return root;
    }
};

LeetCode538

Morris遍历实现中序遍历(LeetCode538:Convert BST to Greater Tree)

给定一棵二叉搜索树(BST),将其转换为一棵更大的树,使得每个节点的值都加上该树上所有大于该树的节点值

Input: The root of a Binary Search Tree like this:
              5
            /   \
           2     13

Output: The root of a Greater Tree like this:
             18
            /   \
          20     13

思路:使用Morris遍历完成中序遍历的逆序遍历,然后在遍历过程中,加上上一个遍历节点的值到当前节点值中。时间复杂度为O(N),空间复杂度为O(1)

代码实现:

/**
 * 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 {
public:
    TreeNode* convertBST(TreeNode* root) {
        //使用中序遍历的逆序遍历
        TreeNode* cur1 = root;//以cur1为基础进行遍历操作
        TreeNode* cur2 = NULL;
        TreeNode* last = NULL;
        while(cur1 !=NULL){
            cur2 = cur1->right;//找到其右子树根节点
            if(cur2 != NULL){
                while (cur2->left!=NULL&&cur2->left!=cur1){
                     cur2 = cur2->left;
                 }
                 //循环停止有两个情况
                 //1.遍历到最左子节点:cur2->right==null
                if(cur2->left == NULL){
                   cur2->left = cur1;//进行morris连接
                   cur1 = cur1->right;//先对所有右子树进行morris处理
                   continue;
                 }else{
                //2.最左节点已经完成morris连接,此时说明cur1的右子树全部完成遍历,对当前节点cur1执行操作,并开始向左子树或向上进行遍历
                   cur2->left = NULL;//调整回来
                 }
           } 
           //如果cur2本身就为空,说明cur1就已经是最右节点也就是最小的节点,此时可以进行打印或执行遍历操作
           if(last != NULL){
               cur1->val += last->val;
           }
           last = cur1;//保存上一个打印的节点
           cur1 = cur1->left;//向左遍历,或者向上,
       }
        
        return root;
    }
};

树ending

LeetCode127

单词接龙(LeetCode127:Word Ladder)

链接

给定两个单词(beginWordendWord)和一个字典,找到从 beginWordendWord 的最短转换序列的长度。转换需遵循如下规则:

  1. 每次转换只能改变一个字母。
  2. 转换过程中的中间单词必须是字典中的单词。

说明:

  • 如果不存在这样的转换序列,返回 0。
  • 所有单词具有相同的长度。
  • 所有单词只由小写字母组成。
  • 字典中不存在重复的单词。
  • 你可以假设 beginWordendWord 是非空的,且二者不相同。

示例 1:

输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

输出: 5

解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
     返回它的长度 5。

示例 2:

输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

输出: 0

解释: endWord "cog" 不在字典中,所以无法进行转换。

思路分析:将所有的单词放在一张图中,所以可以通过一个字母的变化进行转换的两个单词之间具有一条边。使用BFS广度优先遍历,来找到最短变化路径

我的实现:

  1. 使用队列进行广度优先遍历
  2. 使用map记录节点遍历所在层
  3. 逐个将变化过程中的当前单词和wordList中的所有单词进行比较,找出只有一个字母不同的所有单词集合(也就是下一层次的节点集合)
  4. 直到找到搜索目标endWord,然后返回map记录的层次数即可

结果:超时

原因:第三步中寻找下一层次的单词集合过于耗时。

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        
        map <string,int> visited;
        for(auto word:wordList)
            visited.insert(make_pair(word,0));
        
        if(!visited.count(endWord))
            return 0;
        queue<string> que;
        que.push(beginWord);
        
        if(count(wordList.begin(),wordList.end(),beginWord)){
            visited[beginWord] = 1;
        }else{
            visited.insert(make_pair(beginWord,1));
        }
        
        
        
        int word_len = beginWord.size();
        int diff = 0;
        bool found = false;
        int length = 1;
        string cur_word;
        
        while(!que.empty()){
            cur_word = que.front();//队列实现广度优先遍历
            que.pop();
            for(int i = 0;i<wordList.size();i++){//在list寻找所有邻边的下一层次的单词
                if(visited[wordList[i]] == 0){
                    diff = 0;
                    for(int j = 0;j < word_len;j++){
                        if(cur_word[j] != wordList[i][j])
                            diff++;
                        if(diff > 1)
                            break;
                    }
                    if(diff==1){//找到邻边单词,压入队列
                        if(wordList[i] == endWord){//找到搜索目标
                            found = true;
                            visited[wordList[i]] = visited[cur_word]+1;
                            break;
                        }
                        visited[wordList[i]] = visited[cur_word]+1;
                        que.push(wordList[i]);
                        wordList.erase(wordList.begin()+i);
                        i--;// 擦除当前元素后,调整下标
                    }
                    
                }
            }
            if(found)
                break;
        }
        
        return visited[endWord];//返回层次信息
        
    }
    
};

未超时实现:主要优化的是第三步的寻找下一层次节点集合

  • 将变化过程中的当前单词逐个字母(a-z)变化,穷尽所有邻边节点的可能,然后在wordList中进行搜索所有可能节点是否存在。
  • 并且将wordList的单词存放到unordered_set红黑树结构中加快搜索速度
class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> wordDict(wordList.begin(),wordList.end());//利用红黑树的特性提高搜索速度
        
        queue<string> que;
        que.push(beginWord);
        
        if(wordDict.find(endWord) == wordDict.end()){
            return 0;
        }
        int word_len = beginWord.size();
        int diff = 0;
        int length = 0;
        string cur_word;
        
        while(!que.empty()){
            length++;
            for(int k = que.size();k>0;k--){//按层遍历,每一层que.size()固定遍历
                cur_word = que.front();
                que.pop();
                for(int i = 0;i< word_len;i++){
                    char ch = cur_word[i];
                    for(int j = 'a';j <= 'z';j++){//穷尽所有邻边节点的可能单词
                        cur_word[i] = j;
                        if(wordDict.find(cur_word)!=wordDict.end()){//List存在该邻边单词
                            if(cur_word == endWord){
                                return length+1;
                            }
                            que.push(cur_word);
                            wordDict.erase(cur_word);
                        }
                   }
                    cur_word[i] = ch;
                }
                
            }
        }
        
        return 0;
        
    }
    
};

LeetCode130

被围绕的区域(LeetCode130:Surrounded Regions)

给出一个二维数组,其中的元素要么为’X’要么为’O’,将其中被’X’包围的所有’O’替换成’X’。其实就是找出边界上的’O’,除了边界上的’O’和与其区域进行连通的’O’不被替换为’X’,其余都被替换成’X’。

X X X X
X O O X
X X O X
X O X X

输出结果:

X X X X
X X X X
X X X X
X O X X

思路:

  1. 找出四条边界中所有的’O’
  2. 根据找出的’O’进行DFS深度优先遍历,找到所有与其连通的’O’
  3. 将这些’O’替换成’#’
  4. 最后遍历该二维数组,’O’替换为’X’,’#’替换为’O’

代码实现(可以使用递归,也可以使用栈):

其实深度优先遍历就是一种回溯法,可以直接用递归实现。

这里我们使用栈进行实现


class Solution {
public:
    void solve(vector<vector<char>>& board) {
        stack<pair<int,int>> index_stack;
        if(board.empty())
            return;
        int raw_end = board[0].size()-1;
        int col_end = board.size()-1;
        
        if(raw_end<2||col_end<2)
            return;//边界问题的考虑
        
        //初始化visit二维数组,记录被访问信息
        int ** visit = new int*[col_end+1];
        for(int i = 0;i< col_end+1;i++){
            visit[i] = new int[raw_end+1];
        }
        
        for(int i = 0; i< col_end+1;i++){
            for (int j = 0;j < raw_end+1;j++){
                visit[i][j]  = 0;
            }
        }
        
        for(int j = 0;j<raw_end+1;j++){
            if(board[0][j] == 'O')
                index_stack.push(make_pair(0,j));
            if(board[col_end][j] == 'O')
                index_stack.push(make_pair(col_end,j));
        }
        
        for(int i = 1;i<col_end;i++){
            if(board[i][0] == 'O')
                index_stack.push(make_pair(i,0));
            if(board[i][raw_end] == 'O')
                index_stack.push(make_pair(i,raw_end));
        }
        
        //开始深度优先遍历
        
        while(!index_stack.empty()){
            auto curpair = index_stack.top();
            index_stack.pop();
            board[curpair.first][curpair.second] = '#';
            if(!visit[curpair.first][curpair.second]){
                visit[curpair.first][curpair.second] = 1;//设置为已访问
                //向下
                if(curpair.first<col_end && !visit[curpair.first+1][curpair.second] && board[curpair.first+1][curpair.second]=='O'){
                    index_stack.push(make_pair(curpair.first+1,curpair.second));
                }
                //向上
                if(curpair.first>0 && !visit[curpair.first-1][curpair.second] && board[curpair.first-1][curpair.second]=='O'){
                    index_stack.push(make_pair(curpair.first-1,curpair.second));
                }
                
                //向右
                if(curpair.second>0 && !visit[curpair.first][curpair.second-1] && board[curpair.first][curpair.second-1]=='O'){
                    index_stack.push(make_pair(curpair.first,curpair.second-1));
                }
                
                //向左
                if(curpair.second<raw_end && !visit[curpair.first][curpair.second+1] && board[curpair.first][curpair.second+1]=='O'){
                    index_stack.push(make_pair(curpair.first,curpair.second+1));
                }
            }
            
        }
        
        //最后进行替换
        for(int i =0;i<=col_end;i++){
            for(int j = 0;j<=raw_end;j++){
                if(board[i][j] == 'O'){
                    board[i][j] = 'X';
                }else if(board[i][j] == '#'){
                    board[i][j] = 'O';
                }
            }
        }
        
        return;
    }
};

链表

LeetCode138

复制带有随机指针的链表(LeetCode138:Copy List with Random Pointer)

给出一个链表,每个节点带有一个随机指针,指向链表中的任意一个节点,复制该链表

思路:

  1. 在原链表在每个节点后复制一份。
  2. 设置每个节点的随机指针
  3. 分开新旧链表

PS:注意只有单个节点及其随机指针的情况

class Solution {
public:
    RandomListNode *copyRandomList(RandomListNode *head) {
        if(!head) return nullptr;
        RandomListNode* old = head;
        //1.在每个旧节点后复制一个节点
        while(old)
        {
            RandomListNode* tn = new RandomListNode(old->label);
            tn->next = old->next;
            old->next = tn;
            old = tn->next;
        }
        //2.设置每个复制节点的random值
        old = head;
        while(old)
        {
            if(old->random)
                old->next->random = old->random->next;
            old = old->next->next;
        }
        //3.将新旧链表拆开
        old = head;
        RandomListNode* copy = nullptr;
        RandomListNode* curCopy = nullptr;
        while(old)
        {
            if(old == head)
            {
                copy = old->next;
                curCopy = copy;
                old->next = curCopy->next;
            }else
            {
                curCopy->next = old->next;
                old->next = curCopy->next->next;
                curCopy = curCopy->next;
            }
            old = old->next;
        }
        return copy;
    }
};

LeetCode148

链表排序(LeetCode148:Sort List)

给一个链表,将其排序,要求时间复杂度为O(NlogN),空间复杂度为0

我的思路:使用插入排序,时间复杂度超过要求O(N^2)

正确思路:归并排序

分治,将大链表拆分成2个长度相等的小链表,递归处理,每次递归返回后,两个小链表已经有序,然后将2个小链表进行归并

每次将链表分成2个长度相等的链表时,需要找到中间节点,可以使用1个快指针和1个慢指针

如果考虑递归调用的函数栈帧,那么空间复杂度为O(n),不考虑则空间复杂度为O(1)

递归是“从上往下”的思想,也可以“从下往上”,避免函数栈帧的开销

“从下往上”的代码:

/**
 * Merge sort use bottom-up policy, 
 * so Space Complexity is O(1)
 * Time Complexity is O(NlgN)
 * stable sort
*/
class Solution {
public:
    ListNode *sortList(ListNode *head) {
        if(!head || !(head->next)) return head;
        
        //get the linked list's length
        ListNode* cur = head;
        int length = 0;
        while(cur){
            length++;//计算长度
            cur = cur->next;
        }
        
        ListNode dummy(0);
        dummy.next = head;
        ListNode *left, *right, *tail;
        for(int step = 1; step < length; step <<= 1){//步数逐渐增大1->2->4,从小到大增加
            cur = dummy.next;
            tail = &dummy;
            while(cur){
                left = cur;
                right = split(left, step);//根据step进行链表划分
                cur = split(right,step);
                tail = merge(left, right, tail);
            }
        }
        return dummy.next;
    }
private:
    /**
     * Divide the linked list into two lists,
     * while the first list contains first n ndoes
     * return the second list's head
     */
    ListNode* split(ListNode *head, int n){
        //if(!head) return NULL;
        for(int i = 1; head && i < n; i++) head = head->next;
        
        if(!head) return NULL;
        ListNode *second = head->next;
        head->next = NULL;
        return second;
    }
    /**
      * merge the two sorted linked list l1 and l2,
      * then append the merged sorted linked list to the node head
      * return the tail of the merged sorted linked list
     */
    ListNode* merge(ListNode* l1, ListNode* l2, ListNode* head){//合并两个排序链表
        ListNode *cur = head;
        while(l1 && l2){
            if(l1->val > l2->val){
                cur->next = l2;
                cur = l2;
                l2 = l2->next;
            }
            else{
                cur->next = l1;
                cur = l1;
                l1 = l1->next;
            }
        }
        cur->next = (l1 ? l1 : l2);
        while(cur->next) cur = cur->next;
        return cur;
    }
};

LeetCode234

回文链表(LeetCode234:Palindrome Linked List)

给出一个链表,判断该链表是否为回文链表,要求时间复杂度为O(N),空间复杂度为O(1)

思路:

  1. 先用快慢指针找到中间节点
  2. 再讲后半部分进行反转
  3. 最后进行前半部分和后半部分进行比较

由于我的代码比较笨重,这里使用一份参考代码:

/**
 * 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) {
         if (!head || !head->next) return true;
         ListNode* fast = head;
         ListNode* slow = head;
         //找到中间节点
         while (fast->next && fast->next->next)//判断是否可以前进
         {
             fast = fast->next->next;
             slow = slow->next;
         }
         ListNode* reverse = slow->next;
         slow->next = nullptr;
         ListNode* before = nullptr;
         //将后半部分反转
         while (reverse)
         {
             ListNode* next = reverse->next;
             reverse->next = before;
             before = reverse;
             reverse = next;
         }
         //从左右两边向中间靠拢,翻转后的尾节点为null
         bool isOK = true;
         while (before)
         {
             if (head->val != before->val)
             {
                 isOK = false;
                 break;
             }
             head = head->next;
             before = before->next;
         }
         return isOK;
     }
 };

LeetCode237

删除链表给定节点(LeetCode237:Delete Node in a Linked List)

给出一个链表中节点,删除该链表中的该节点,该节点不会是该链表的最后一个节点。

Input: head = [4,5,1,9], node = 5
Output: [4,1,9]
Explanation: You are given the second node with value 5, the linked list
             should become 4 -> 1 -> 9 after calling your function.

思路:将该节点的后面所有节点的值向前移动一个节点,再将倒数第二个节点的next指向空

代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void deleteNode(ListNode* node) {
        while(node->next->next!=NULL){
            node->val = node->next->val;
            node = node->next;
        }
        
        node->val = node->next->val;
        node->next = NULL;
    }
};

LeetCode328

奇偶链表(LeetCode328:Odd Even Linked List)

给定一个链表,将所有奇数位置的链表放到链表前面,偶数位置的链表放在链表后端,且不打乱之前奇数位置节点顺序以及偶数位置上的节点顺序

Input: 1->2->3->4->5->NULL
Output: 1->3->5->2->4->NULL

Input: 2->1->3->5->6->4->7->NULL
Output: 2->3->6->7->1->5->4->NULL

思路:遍历一遍,直接拆开为两个链表即可

代码实现:

class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        if(head == NULL)
            return NULL;
        ListNode* oddhead = head,*oddtail = head;//奇数链表头,尾
        ListNode* evenhead,*eventail;//偶数链表头,尾
        if(head->next){
             evenhead = head->next;
             eventail = head->next;
        }  
        else
            return head;
        
        ListNode* cur = eventail->next;
        
        while(cur){//开始遍历
            oddtail->next = cur;
            oddtail = cur;
            if(cur->next){
                eventail->next = cur->next;
                eventail = cur->next;
            }else{
                eventail->next = NULL;
                break;
            }
            cur = cur->next->next;
        }      
        oddtail->next = evenhead;
        return oddhead;
        
    }
};

链表ending

数据结构

LeetCode150

求得逆波兰表达式的值(LeetCode150:Evaluate Reverse Polish Notation)

逆波兰表达式:后缀表达式。在通常的表达式中,二元运算符总是置于与之相关的两个运算对象之间,这种表示法也称为中缀表示。所以逆波兰表达式的二元运算符位于与之相关的两个运算对象之后

Input: ["2", "1", "+", "3", "*"]
Output: 9
Explanation: ((2 + 1) * 3) = 9

Input: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
Output: 22
Explanation: 
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

思路:使用栈结构即可


class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> nums;
        for(int i = 0;i< tokens.size();i++){
            
            int tmp;
            if(tokens[i] == "+"){
                tmp = nums.top();
                nums.pop();
                tmp += nums.top();
                nums.pop();
            }else if(tokens[i] == "-"){
                tmp = nums.top();
                nums.pop();
                tmp = nums.top()-tmp;
                nums.pop();
            }else if(tokens[i] == "*"){
                tmp = nums.top();
                nums.pop();
                tmp *= nums.top();
                nums.pop();
            }else if(tokens[i] == "/"){
                tmp = nums.top();
                nums.pop();
                tmp = nums.top()/tmp;
                nums.pop();
            }else{
                tmp = stoi(tokens[i],nullptr,10);
            }
            
            nums.push(tmp);
        }
        
        return nums.top();
    }
};

LeetCode239

滑动窗口最大值(LeetCode239: Sliding Window Maximum)

给出一个数组和一个窗口大小,该窗口在数组结构滑动过程中的所有最大值

Input: nums = [1,3,-1,-3,5,3,6,7], and k = 3
Output: [3,3,5,5,6,7] 
Explanation: 

Window position                Max
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

思路:双端队列 ,见剑指offer:面试题59:队列的最大值

代码实现:


class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<pair<int,int>> record;
        vector<int> max_vec;
        if(nums.empty())
            return max_vec;
        record.push_back(pair<int,int>(0,nums[0]));
        if(k == 1){//因为从i=1开始,所以要考虑k==1的情况
            max_vec.push_back(nums[0]);
        }
        for(int i = 1; i< nums.size();i++){
            if(record.back().second>=nums[i]){
                record.push_back(pair<int,int>(i,nums[i]));//当队尾元素大于当前元素,直接压入
            }else{
                while(!record.empty()&&record.back().second<nums[i]){//弹出队尾所有小于当前元素的值
                    record.pop_back();
                }
                record.push_back(pair<int,int>(i,nums[i]));
            }
            
            if(i>=k-1){//当滑到第k个元素时,开始记录最大值
                if(record.front().first<i-k+1){
                    record.pop_front();
                }
                max_vec.push_back(record.front().second);        
            }  
        }
        
        return max_vec;
    }
};

LeetCode295

数据流中的中位数(LeetCode295:Find Median from Data Stream)

实现一个类,可以添加数据,可以获取所有添加数据中的中位数

addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3) 
findMedian() -> 2

思路:见剑指offer面试题41:数据流中的中位数

  1. 用数组vector实现
  2. 用链表实现
  3. 用二叉搜索树实现
  4. 用堆来实现,大小堆

我的实现(链表)

struct ListNode_x{
    public:
        ListNode_x *prev;
        ListNode_x *next;
        double val;
        ListNode_x(int value):val(value),prev(NULL),next(NULL){}
};

class MedianFinder {
public:
    /** initialize your data structure here. */
    MedianFinder() {
        head = new ListNode_x(NULL);
        mid = head;
        count = 0;
    }
    
    void addNum(int num) {
        ListNode_x *cur = head;
        while(cur->next!=NULL&&cur->next->val<=num) cur = cur->next;
        if(cur->next!=NULL){
            ListNode_x *nextptr = cur->next;
            cur->next = new ListNode_x(num);
            cur->next->prev = cur;
            cur->next->next = nextptr;
            nextptr->prev = cur->next;
        }else{
            cur->next = new ListNode_x(num);
            cur->next->prev = cur;
        }
        
        if(mid == head){
            mid = mid->next;
        }else{
            if(count&1){
                if(mid->val>num)
                    mid = mid->prev;
            }else{
                if(mid->val<=num)
                    mid = mid->next;
            }
        }
        count++;
    }
    
    double findMedian() {
        if(mid!=head)
            if(count&1)
                return mid->val;
            else
                return (mid->val+mid->next->val)/2;
        else
            return NULL;
    }
    ListNode_x *head;
    ListNode_x *mid;
    int count;
    
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

实现2:用堆来实现

class MedianFinder {
public:
    /** initialize your data structure here. */
    MedianFinder() : count(0){}
    
    void addNum(int num) {
        ++count;
        int temp = num;
        if(count%2==1) //奇数个
        {
            if(!rightHeap.empty() && num>rightHeap.top())
            {
                temp = rightHeap.top();
                rightHeap.pop();
                rightHeap.push(num);
            }
            leftHeap.push(temp);
        }
        else //偶数个
        {
            if(!leftHeap.empty() && num<leftHeap.top())
            {
                temp = leftHeap.top();
                leftHeap.pop();
                leftHeap.push(num);
            }
            rightHeap.push(temp);
        }
    }
    
    double findMedian() {
        if(leftHeap.size()==rightHeap.size())
            return (leftHeap.top()+rightHeap.top()) / 2.0;
        else
            return leftHeap.top();
    }
private:
    //最大堆
    priority_queue<int, vector<int>, less<int>> leftHeap;
    //最小堆
    priority_queue<int, vector<int>, greater<int>> rightHeap;
    int count;
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

LeetCode347

前K个高频元素(LeetCode347:Top K Frequent Elements)

给定一个元素,其中有部分元素出现重复,根据每个元素重复出现的次数,来获取前K个重复出现次数最多的元素。

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

要求时间复杂度为: O(n log n)

思路实现:

  1. 先用红黑树结构map统计每个元素重复出现的次数
  2. 再用multimap结构并以重复出现的次数为key值来对之前统计的map结构中的元素进行排序
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        map<int,int> fre_map;
        for(int i = 0;i<nums.size();i++){//将所有元素统计重复出现的次数
            if(fre_map.find(nums[i]) == fre_map.end()){
                fre_map.insert(pair<int,int>(nums[i],1));
            }else{
                fre_map[nums[i]]++;
            }
        }
        
        multimap<int,int> topkmap;
        
        for(auto p:fre_map){
            topkmap.insert(pair<int,int>(p.second,p.first));//以重复出现的次数为key值,来讲所有统计的元素插入到multimap中进行排序
        }
        vector<int> res;
        auto pairtmp = --topkmap.end();
        while(k){
            res.push_back(pairtmp->second);//排序后,倒序输出前k个高频元素
            k--;
            pairtmp--;
        }
        
        return res;
    }
};

LeetCode350

求两个数组的交集(LeetCode350:Intersection of Two Arrays II)

给定两个数组,求两个数组的交集

Given nums1 = [1, 2, 2, 1], nums2 = [2, 2], return [2, 2].

进阶:

  1. 如果给定的数组都是排好序的,怎么样优化你的算法
  2. 如果第一个数组的大小小于第二个数组,哪个算法会更好
  3. 如果数组2存放在磁盘上,内存有一定限制,不能将数组2的数据一次性加载到磁盘,要怎么做?

普通解法思路: 使用红黑树结构

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        multiset<int> record1;
        vector<int> res;
        for(int i=0;i<nums1.size();i++){
            record1.insert(nums1[i]);//保存所有数组1中的元素到红黑树结构中
        }
        
        for(int i=0;i<nums2.size();i++){
            auto iter = record1.find(nums2[i]);//匹配一个则在红黑树中删除一个元素
            if(iter!=record1.end()){
                record1.erase(iter);
                res.push_back(nums2[i]);
            }  
        }
        return res;
    }
};

LeetCode378

排好序的二维数组中求第k大的元素

给一个二维数组,该二维数组中的每行和每列都保持递增关系,求该二维数组中的第k小的元素

matrix = [
   [ 1,  5,  9],
   [10, 11, 13],
   [12, 13, 15]
],
k = 8,

return 13.

思路:将该二维(m,n)数组看做m条升序链表,然后合并,这个题目的思路则可以参考LeetCode23: Merge k Sorted Lists

将这m条链表中的首元素构建一个最小堆,优先级队列,然后找出其中最小的元素,然后将该元素的下一个元素加入到该最小堆中,如此循环k次就可以找到第k小的元素了。

PS:

  1. 优先级队列的使用priority_queue
  2. 优先级队列如何将默认的最大堆改为最小堆
  3. 优先级队列如何自定义比较操作符

代码实现:

struct Element{
    int value;
    int col;
    int raw;
    Element(int val,int i,int j):value(val),col(i),raw(j){}
    friend bool operator > (const struct Element &n1, const struct Element &n2) ;
};

inline bool operator > (const struct Element &n1, const struct Element &n2) {
    return n1.value > n2.value;
}

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        priority_queue<Element,vector<Element>,greater<Element>> q;//构建最小堆的优先级队列,并且传入自定义比较操作符
        for(int i=0;i<matrix.size();i++){
            q.push(Element(matrix[i][0],i,0));
        }
        
        int res;
        while(k--){//循环k次
            Element e = q.top();//获取堆定最小元素并弹出
            q.pop();
            if(e.raw<matrix[e.col].size()-1){
                q.push(Element(matrix[e.col][e.raw+1],e.col,e.raw+1));//压入弹出元素的下一个元素
            }
            res = e.value;
        }
        
        return res;
        
        
    }
};

LeetCode380

常数时间完成对数据插入,删除和随机返回

实现一个随机集合类,该类结构可以在O(1)的时间内完成如下操作:

  1. 插入数据,如果该数据已经存在插入失败返回false,否则插入成功,返回true
  2. 删除数据,如果删除的数据不存在,则删除失败返回false,否则删除成功返回true
  3. 随机返回一个数,在已有的数据集合中,随机返回一个数,保证所有数被返回的概率相等
// Init an empty set.
RandomizedSet randomSet = new RandomizedSet();

// Inserts 1 to the set. Returns true as 1 was inserted successfully.
randomSet.insert(1);

// Returns false as 2 does not exist in the set.
randomSet.remove(2);

// Inserts 2 to the set, returns true. Set now contains [1,2].
randomSet.insert(2);

// getRandom should return either 1 or 2 randomly.
randomSet.getRandom();

// Removes 1 from the set, returns true. Set now contains [2].
randomSet.remove(1);

// 2 was already in the set, so return false.
randomSet.insert(2);

// Since 2 is the only number in the set, getRandom always return 2.
randomSet.getRandom();

思路:

  1. 插入要在常数时间内完成,并判断该数据是否存在,需要使用hash表结构
  2. 要在常数时间内返回一个随机的数,只能使用数组结构vector
  3. 所以我们实现一个hashmap-vector的两层索引结构,hashmap中存放对应val在vector中的下标
  4. 由于vector在删除元素后,部分元素的下标会失效。所以我们删除元素时应该将被删除的元素先和vector尾部元素交换,然后就只需要调整原尾部元素的下标即可。然后删除现在在尾部的元素。

代码实现:

class RandomizedSet {
public:
    /** Initialize your data structure here. */
    RandomizedSet() {
        
    }
    
    /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
    bool insert(int val) {
        if(map.find(val) != map.end()){
            return false;
        }
        nums.push_back(val);
        map[val] = nums.size()-1;
        return true;
    }
    
    /** Removes a value from the set. Returns true if the set contained the specified element. */
    bool remove(int val) {
        if(map.find(val) == map.end()){
            return false;
        }
        swap(nums[map[val]],nums.back());//和尾部元素交换
        nums.pop_back();
        map[nums[map[val]]] = map[val];//调整原尾部元素的下标
        map.erase(val);
        return true;
    }
    
    /** Get a random element from the set. */
    int getRandom() {
        int index = rand()%nums.size();
        return nums[index];
    }
    unordered_map<int,int> map;
    vector<int> nums;
};

/**
 * 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();
 */

LeetCode454

四数求和(LeetCode454:4SumII)

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

输出:
2

解释:
两个元组如下:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

思路,先用一个哈希表记录前两个数组中任意两个元素可能出现的和,然后在后两个数组中查找是否有两个数的和等于该hash表中出现的和的相反数即可。

class Solution {
public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
        int count = 0;
        
        unordered_map<int,int> map;
        
        for(int num1 : A)
            for(int num2 : B)
                map[num1+num2]++;
        
        for(int num3 : C)
            for(int num4 : D){
                auto itr = map.find(-num3-num4);
                if(itr != map.end())
                    count += itr->second;
            }
        
        return count;  
    }
};

code 452

设计一个没有扩容负担的的堆结构

堆结构一般是使用固定长度的数组结构来实现的,这样的实现虽然足够经典,但存在扩容的负担,比如不断向堆中增加元素,使得固定数组快耗尽时,就不得不申请一个更大的固定数组,然后把原来数组中的对象复制到新的数组中完成扩容,所以如果扩容时堆中的元素个数为N,那么扩容行为的时间复杂度为O(N)。请设计一个没有扩容负担的堆结构,即在任何时刻有关堆的操作时间复杂度都不超过O(logN)。

实现接口:

  • getHead:O(1)获取堆顶的值
  • getSize:O(1)返回当前堆的大小
  • add(x):O(logN)即向堆中新加元素,操作后依然是小根堆/大根堆
  • popHead:O(logN)删除并返回堆顶的值

思路:使用二叉树结构,比经典的二叉树节点多一条指向父节点的parent指针:

class Node<K>{
public:
    K value;
    Node<K> *left;
    Node<K> *right;
    Node<K> *parent;
}

堆的数据结构用一个Myheap类表示:其中有四个重要的结构

  • head:指向当前的堆顶节点
  • last:指向当前堆的最后一个节点
  • size:当前堆的大小
  • comp:继承了Comparator接口的比较器类型的变量

所以堆的插入操作,需要结合last的位置来确定:

  • 情况1(图9-7):last是当前所在层的最后一个节点,那么插入位置在下一层的第一个节点
  • 情况2(图9-8):last是last父节点的左孩子,那么newNode应该加在last父节点的右孩子的位置
  • 情况3(图9-9):如果既不是情况1也不是情况2,则要向上寻找,寻找到第一个位于其左子树上的父节点,然后找到该父节点右子树上的最左节点插入。

myheap1 myheap2

堆顶的删除操作:将last所指向的节点设置为head,删除掉原head,修改size,然后执行堆的下沉sink操作

数据结构ending

程序设计类

LintCode660

一次读取4字节的接口实现读取N字节的接口

Read N Characters Given Read4 II - Call multiple times 接口:int read4(char * buf)一次从文件中读取 4 个字符。 返回值是实际读取的字符数。 例如,如果文件中只剩下 3 个字符,则返回 3。 通过使用read4 接口,实现从文件读取 n 个字符的函数int read(char * buf,int n)。

注意事项 read 函数可能被调用多次。

思路:分情况

  1. 需要缓冲区保留一次读取4字节的数据,当读取的数据少于4字节时,说明还有部分数据遗留在缓冲区中
  2. 当缓冲区有数据时,分为两类
    • 当读取的数据少于缓冲区中的数据时,则从缓冲区读取数据即可,然后调整缓冲区剩余数据的区间
    • 当读取的数据多于缓冲区的数据时,则先将缓冲区的剩余数据读取出来
  3. 缓冲区的数据为空时时,分为两类:
    • 读取的数据少于4字节,则需要先读到缓冲区,然后读取指定数据的字节,缓冲区保存剩余数据
    • 读取的数据大于4字节,直接循环读取4字节到目标地址。

代码实现:

int read4(char *buf);
#include<cstring>
class Solution {
public:
    /**
     * @param buf destination buffer
     * @param n maximum number of characters to read
     * @return the number of characters read
     */
    char help[4];
    int start =0,end=0;
    int read(char* buf, int n) {
        // Write your code here
        int k = n;
        int count = 0;
        char* buftmp = buf;
        if(k<end-start){
            //当读取数据小于缓冲区中的数据时
            memcpy(buftmp,help+start,k);
            start +=k;
            count +=k;
        }else{
            if(end-start>0){
                //读取缓冲区中剩余数据
                memcpy(buftmp,help+start,end-start);
                k -= end-start;
                count += end-start;
                start = 0;
                end = 0;
            }
            //当剩余读取的数据大于4时
            while(k>4){
                int res = 0;
                res = read4(buftmp+count);
                if(res){
                    k -=res;
                    count += res;
                }else{
                    return count;
                }
            }
            
            //处理尾部数据
            int tmp = read4(help);
            if(tmp>k){
                memcpy(buftmp+count,help,k);
                start = k;
                end = tmp;
                count +=k;
                return count;//全部成功读取
            }else{
                memcpy(buftmp+count,help,tmp);
                count += tmp;
                return count;
            }
            
        }
    }
};

其它

LeetCode11

装水最多的容器(LeetCode11:Container with most water)

描述:给定一个整型数组{a1,a2,a3…an};这些点代表在坐标轴上的n个点,(i,ai)。根据这些点画出垂直于x轴的直线,这些直线中任取两条直线构成一个容器,求两条直线使得构成的容器可以装水的体积最大。

container_water

如图灰色部分就是两条加粗的线构成容器的所装水的容量。

思路:两点法,从两端开始front =1 ,back = end,构成容器的大小为size,只要比较这两条直线的长度,在front和back的范围里想要找到另一条线和这两条线其中一条构成的容器大于size,则只能和较长的一端进行组合,所以我们将较短的一端向中间移动找到一条比它长的直线作为容器的一端,然后计算容器大小,如此循环移动直到front和back重合,在这个过程中记录容器容量的最大值

解法链接

代码参考:

class Solution {
public:
    int maxArea(vector<int>& height) {
        if(height.empty()||height.size()==1){
            return 0;
        }
        int front = 0,back = height.size()-1;
        int max_size = (height[back]>height[front]?height[front]:height[back])*(back-front);
        //移动直到两端重合
        while(front<back){
            int front_tmp = front;
            int back_tmp = back;
            int h = 0;
            //向中间移动较短的一端
            if(height[front]>=height[back]){
            //将尾端向前移动
                while(height[back_tmp] >= height[back] && back>front)
                    back--;
                if(height[back_tmp]<height[back]){
                //计算容器大小,记录最大容器容量
                    h = height[back]>height[front]?height[front]:height[back];
                    max_size = h*(back-front)>max_size?h*(back-front):max_size;
                }
                    
            }else{
            //将首段向后移动
                while(height[front_tmp] >= height[front] && back>front)
                    front++;
                if(height[front_tmp] < height[front]){
                    h = height[back]>height[front]?height[front]:height[back];
                    max_size = h*(back-front)>max_size?h*(back-front):max_size;
                }
            }
            
        }
        
        return max_size;
        
    }
};

LeetCode42

计算容器可以装多少水(LeetCode42:Trapping Rain Water)

描述:给出一个整形数数组,每个数代表一个柱子,柱子的宽度为1,两根柱子之间可以装水,问该数组中的所有数字构成的所有柱子能装多少水?

rainwatertrap

如上图,给出数组[0,1,0,2,1,0,1,3,2,1,2,1],输出:6(装水的容量)

我的解法:笨拙复杂,使用两个栈,一个栈记录高度,一个栈记录该减去的水的容量,其实和标准的使用栈解法的思路类似,只不过思路过于复杂。

  • 我的思路和标准解法的思路都是求两根柱子,超出其他矮柱子的高度的部分装的水的容量,这部分的水就只和这两根柱子相关,和其他的柱子没有关系,所以就不会导致重复计算。
    • 但我的思路是先求两根柱子所能装的水的最大容量,然后减去两根柱子中间被其他柱子所影响的容量,这就比较复杂
    • 标准解法的思路就比较直接,直接求超出的高度,计算只与这两根柱子相关的水容量
class Solution{
    public:
    int trap(vector<int>& height) {
        if(height.empty()||height.size()==1){
            return 0;
        }
        
        stack<pair<int, int>> height_stack;
        stack<int>  minus_stack;
        int water_tmp = 0,water_sum = 0,water_max = 0;
        int minus_tmp = 0;
        
        for(int i = 0;i< height.size();i++){
            if(height[i]!=0){
                if(height_stack.empty()){
                
                    height_stack.push(make_pair(height[i], i));
                    minus_stack.push(0);
                }else{
                    while(!height_stack.empty()){
                        pair<int, int> pair_tmp = height_stack.top();
                        if(pair_tmp.first > height[i]){
                            minus_tmp = minus_stack.top();
                            water_tmp = height[i]*(i-pair_tmp.second-1)-minus_tmp;
                            water_sum += water_tmp;
                            minus_tmp +=water_tmp;
                            minus_stack.pop();
                            minus_stack.push(minus_tmp);
                            break;
                        }
                        minus_tmp = minus_stack.top();
                        water_tmp = pair_tmp.first*(i-pair_tmp.second-1)-minus_tmp;//只与目前的两根柱子相关的水容量
                        water_max = water_tmp + minus_tmp;
                        water_sum += water_tmp;
                        height_stack.pop();
                        minus_stack.pop();
                        if(!height_stack.empty()){
                            minus_tmp = minus_stack.top();
                            minus_stack.pop();
                            minus_tmp = minus_tmp + water_max + pair_tmp.first;
                            minus_stack.push(minus_tmp);
                        }
                    }
                    height_stack.push(make_pair(height[i], i));
                    minus_stack.push(0);
                }
            }
        }
        
        return water_sum;
    }
};
  • 思路1: 标准的栈解法:
int trap(vector<int>& height)
{
    int ans = 0, current = 0;
    stack<int> st;
    while (current < height.size()) {
        while (!st.empty() && height[current] > height[st.top()]) {
            int top = st.top();
            st.pop();//当压入一个数时,会把所有小于该数的数全部弹出栈
            if (st.empty())
                break;
            int distance = current - st.top() - 1;
            int bounded_height = min(height[current], height[st.top()]) - height[top];//求超出的高度部分,必须有三个数参数,两个相邻的数是无法装水的
            ans += distance * bounded_height;//直接计算超出的高度部分所装的水容量
        }
        st.push(current++);
    }
    return ans;
}
  • 思路2: 计算每一根柱子的顶部空间有多少会被淹没在水里,这样计算每根柱子顶部被淹没在顶部的空间,然后累加起来就是整个水的容量。

    • 解法1:暴力计算,为每根柱子计算左右两边的最高的高度,然后用左右最高高度中的较小者减去该柱子的高度,就是该柱子顶部被淹没的空间
    • 解法2:动态规划,先记录每根柱子的左右最高高度到两个vector中,这样可以避免重复计算。
//解法1:
int trap(vector<int>& height)
{
    int ans = 0;
    int size = height.size();
    for (int i = 1; i < size - 1; i++) {
        int max_left = 0, max_right = 0;
        for (int j = i; j >= 0; j--) { //Search the left part for max bar size
            max_left = max(max_left, height[j]);
        }//计算左边的最高高度
        for (int j = i; j < size; j++) { //Search the right part for max bar size
            max_right = max(max_right, height[j]);
        }//计算右边的最高高度
        ans += min(max_left, max_right) - height[i];//累加每根柱子顶部别淹没的空间
    }
    return ans;
}

//解法2:

int trap(vector<int>& height)
{
    if(height == null)
        return 0;
    int ans = 0;
    int size = height.size();
    vector<int> left_max(size), right_max(size);
    left_max[0] = height[0];
    for (int i = 1; i < size; i++) {
        left_max[i] = max(height[i], left_max[i - 1]);
    }//记录每根柱子左边的最高高度到left_max
    right_max[size - 1] = height[size - 1];
    for (int i = size - 2; i >= 0; i--) {
        right_max[i] = max(height[i], right_max[i + 1]);
    }//记录每根柱子右边的最高高度到right_max
    for (int i = 1; i < size - 1; i++) {
        ans += min(left_max[i], right_max[i]) - height[i];//累加每根柱子顶部被淹没的部分
    }
    return ans;
}

思路三:两点法,从两端找到左边最大的和右边最大的,然后两端向中间移动,遇到短的就可以计算水的容量。https://leetcode.com/problems/trapping-rain-water/solution/

int trap(vector<int>& height)
{
    int left = 0, right = height.size() - 1;
    int ans = 0;
    int left_max = 0, right_max = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            height[left] >= left_max ? (left_max = height[left]) : ans += (left_max - height[left]);
            ++left;
        }
        else {
            height[right] >= right_max ? (right_max = height[right]) : ans += (right_max - height[right]);
            --right;
        }
    }
    return ans;
}

LeetCode13

罗马数转化为整数(LeetCode13:Roman to Integer)

Symbol       Value
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

如上所示,罗马表示数字的字符一一对应,数字规则如下:

  • 规则1:较小字符可以放在较大字符前面构成一个数:如IX(10-1 = 9),XLC(100-50-10 = 40)
  • 规则2:将一串罗马字符串根据规则1得到若干数字然后进行相加如:“MCMXCIV”,得到数字: M=1000,CM=900,XC=90,IV=4。最后相加结果为:1000+900+90+4 = 1994

思路1:从正面将所有数字分开,然后再一个个相加

class Solution {
public:
    int romanToInt(string s) {
        if(s.empty())
            return 0;
        int result = 0;
        map<char,int> roman;
        roman['I'] = 1;
        roman['V'] = 5;
        roman['X'] = 10;
        roman['L'] = 50;
        roman['C'] = 100;
        roman['D'] = 500;
        roman['M'] = 1000;
        
        int num = roman[s[0]];
        int size = s.size();
        for(int i = 1;i<size;i++){
            //分割字符串得到数字
            while(i < size && roman[s[i]] >= roman[s[i-1]]){
                if(roman[s[i]] == roman[s[i-1]]){
                    num += roman[s[i]];
                }else{
                    num = roman[s[i]] - num;
                }
                i++;
            }
            //将得到的数字相加
            result+=num;
            num = 0;
            
            if(i<size){
                num = roman[s[i]];
            }
        }
        if(num != 0)
            result+=num;
        return result;
    }
};

思路2:从反方向来进行遍历,因为字符串构成数字的方向也是从后到前构成的:只有当后面的字符大于前面时才能构成一个数字。

int romanToInt(string s) 
{
    unordered_map<char, int> T = { { 'I' , 1 },
                                   { 'V' , 5 },
                                   { 'X' , 10 },
                                   { 'L' , 50 },
                                   { 'C' , 100 },
                                   { 'D' , 500 },
                                   { 'M' , 1000 } };
                                   
   int sum = T[s.back()];
   //从后向前遍历即可
   for (int i = s.length() - 2; i >= 0; --i) 
   {
       if (T[s[i]] < T[s[i + 1]])
       {
           sum -= T[s[i]];
       }
       else
       {
           sum += T[s[i]];
       }
   }
   
   return sum;
}

LeetCode15

三个数相加为0(LeetCode15:3Sum)

给定一个数组,从中找到所有可能的三元组,使得这三个加起来的总和为0。

思路1(暴力求解):首先将数组进行排序,然后三次循环遍历确定找到使和为0的三元组。 - 优化:最后一次循环可以用关联容器来确定,将所有的数据放到一个关联容器multiset,在前两次循环后,三元组中的第三个数也就可以确定下来,我们可以直接判断该数是否在multiset存在

代码见LeetCode15.3Sum

思路2(O(N^2)):同样需要先将数组进行排序,然后确定三元组中的第一个元素(一次循环),然后我们可以知道后两个元素的和,然后利用两点法(见剑指offer面试题57:和为s的数字),来确定后两个元素的值。

代码(Java):

public List<List<Integer>> threeSum(int[] num) {
    Arrays.sort(num);
    List<List<Integer>> res = new LinkedList<>(); 
    for (int i = 0; i < num.length-2; i++) {
        if (i == 0 || (i > 0 && num[i] != num[i-1])) {
            int lo = i+1, hi = num.length-1, sum = 0 - num[i];
            while (lo < hi) {
                if (num[lo] + num[hi] == sum) {
                    res.add(Arrays.asList(num[i], num[lo], num[hi]));
                    while (lo < hi && num[lo] == num[lo+1]) lo++;
                    while (lo < hi && num[hi] == num[hi-1]) hi--;
                    lo++; hi--;
                } else if (num[lo] + num[hi] < sum) lo++;
                else hi--;
           }
        }
    }
    return res;
}

LeetCode22

产生所有可能的有效括号组合(LeetCode22:Generate Parentheses)

题目描述:给定一个数字n,给出n个括号所有有效的组合方式。如给定3,则输出结果是:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

回溯法:见剑指offer中机器人的运动范围,这个题目也可以用一样的思路来考虑:

tracing_back.png

和机器人走格子类似,横坐标是’(‘的已使用个数,纵坐标是’)’的已使用个数,机器人走到的每一个格子都必须满足横坐标x>=纵坐标y。求有多少条路径满足从start走到end格子。

回溯法和递归的区别:

  • 回溯不断调用自身,调用到最后即得到问题的答案。(将问题不断深入,穷尽每一种可能)
  • 递归则是不断调用自身,直到最小问题得到解决后,在通过不断返回解决大问题,返回到第一次调用的函数才得到答案。(将问题不断分解,解决最小问题,再逐个解决大问题,最后得到整个问题的答案。)

本题代码:

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        string str = "(";
        vector<string> result;
        generateParenthesis_x(result,n-1,n,str);
        return result;
        
    }
    
    void generateParenthesis_x(vector<string> & vec,int s_count,int e_count,string &str){
        if(s_count<e_count&&s_count>0){//两个方向
            string tmp1 = str;
            tmp1.push_back('(');
            generateParenthesis_x(vec,s_count-1,e_count,tmp1);
            string tmp2 = str;
            tmp2.push_back(')');
            generateParenthesis_x(vec,s_count,e_count-1,tmp2);
            return;
        }else if(s_count<e_count&&s_count==0){//一个方向
            string tmp3 = str;
            tmp3.push_back(')');
            generateParenthesis_x(vec,s_count,e_count-1,tmp3);
        }else if(s_count==e_count&&s_count>0){//一个方向
            string tmp4 = str;
            tmp4.push_back('(');
            generateParenthesis_x(vec,s_count-1,e_count,tmp4);
        }else{
            vec.push_back(str);//走到end
        }
        
    }
};

LeetCode23

合并k个排序链表(LeetCode23: Merge k Sorted Lists)

将k个排序链表合并成一个排序链表:

Input:
[
  1->4->5,
  1->3->4,
  2->6
]
Output: 1->1->2->3->4->4->5->6

解决方法:https://leetcode.com/problems/merge-k-sorted-lists/solution/

思路1:用优先级队列,或红黑树实现的multimap结构保存每条链表的第一个元素,然后从这些元素中获取最小值(基于multimap或优先级队列很容易实现)插入新的链表。

PS:注意当创建来的链表为空时的处理

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        multimap<int,ListNode*> first_map;
        ListNode* new_list = NULL;
        ListNode* cur= NULL;
        ListNode* cur_ptr = NULL;
        for(int i=0;i<lists.size();i++){
            if(lists[i]!=NULL)
                first_map.insert(make_pair(lists[i]->val,lists[i]));
        }
        //初始化链表头
        if(!first_map.empty()){
            auto iter0 = first_map.begin();
            new_list = cur = new ListNode(iter0->first);
            cur_ptr = iter0->second;
            first_map.erase(iter0);
            if(cur_ptr->next!=NULL){
                first_map.insert(make_pair(cur_ptr->next->val,cur_ptr->next));
        }
        //每获取一个最小值,需要将对应链表下一个最小值添加到map中来
        while(!first_map.empty()){
            auto iter = first_map.begin();
            ListNode *tmp = new ListNode(iter->first);
            cur->next = tmp;
            cur = tmp;
            cur_ptr = iter->second;
            first_map.erase(iter);
            if(cur_ptr->next!=NULL){
                first_map.insert(make_pair(cur_ptr->next->val,cur_ptr->next));
            }
            
        }
        }
        
        return new_list;
    }
};

思路2:暴力求解,逐个将两条链表合并成一条,直到所有链表都被合并

思路3:归并法,将所有链表分为两两一组,然后合并之后,继续分组合并。。直到最后合并成一条链表

LeetCode55

跳跃游戏(LeetCode55: Jump Game)

给一个非负数组,每个元素中的数值代表可以向前跳跃的最大步数,从第一个元素开始起跳,在每个元素的跳跃步数的限制下问能否跳到最后一格。

Input: [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

思路1:关注为0的元素,因为只要不能到达,肯定是有一个跳跃步数为0的元素限制了向前跳跃的步伐。所以我们遍历该数组,并记录遍历到当前元素可以到达的最远距离,只要遇到0时,判断当前可以到达的距离是否可以越过该0节点。不能越过则说明无法到达终点。

需要注意的特殊情况:0点出现在数组的最后一个元素上。

思路2:动态规划,可以实现但较复杂

思路1的代码实现:

class Solution {
public:
    bool canJump(vector<int>& nums) {
        if(nums.empty()||nums.size() == 1){
            return true;
        }
        bool success = true;//最远距离
        int max_reach = 0,cur_reach = 0;
        int len = nums.size();
        for(int i = 0;i<len;i++){
            if(nums[i]==0 && max_reach<=i && i!=len-1){//遇到0时,判断是否可以越过,并判断是否为最后一个元素
                success = false;
                break;
            }
            cur_reach = i+nums[i];
            if(cur_reach > max_reach)
                max_reach = cur_reach;//更新可以到达的最远距离
        }
        
        return success;
        
    }
};

Jump GameII

跳跃游戏II

给定一个整型数组arr,arr[i] == k代表当前位置i可以向右跳跃的最远距离。如果从位置0出发,返回最少跳跃几次能跳到最后的位置上。

输入:arr = [3,2,3,1,1,4];//不考虑不合法的输入

输出:2

思路:动态规划

  1. 遍历一边数组,记录当前跳数可以达到的最远距离cur,以及cur之前的位置的下一跳可以达到的最远距离next
  2. 当遍历的元素超过cur,相应的跳数jump也会加1,因为jump到达的最远距离是cur,cur之后的元素需要下一跳到达
  3. 超过cur后,当前跳数加1,能到达的最远距离也被更新cur = next
  4. 遍历结束后,得到的跳数就是最少的跳数。

class Solution{
public:
    int jump(vector<int> arr){
        if(arr.empty())
            return 0;
        int jump = 0,cur = 0,next = 0;

        for(int i = 0;i<arr.size();i++){
            if(cur<i){
                jump++;
                cur = next;
            }

            next = max(next,i+carr[i]);
        }

        return jump;

    }
};

LeetCode56

区间合并(LeetCode56: Merge Intervals)

给出一个区间数组,将所有具有重叠情况的数组合并到一个数组,再输出一个没有重叠现象的数组

Input: [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: Since intervals [1,3] and [2,6] overlaps, merge them into [1,6].

Input: [[1,4],[4,5]]
Output: [[1,5]]
Explanation: Intervals [1,4] and [4,5] are considerred overlapping.

思路:先排序,再逐个进行合并。

注意:尾部的处理,不能忘记最后一个区间的处理

/**
 * Definition for an interval.
 * struct Interval {
 *     int start;
 *     int end;
 *     Interval() : start(0), end(0) {}
 *     Interval(int s, int e) : start(s), end(e) {}
 * };
 */
class Solution {
public:
    vector<Interval> merge(vector<Interval>& intervals) {
        vector<Interval> result;
        if(intervals.empty()){
            return result;
        }
        
        multimap<int,Interval> in_map;
        int size = intervals.size();
        for(int i = 0;i<size;i++){
            in_map.insert(make_pair(intervals[i].start,intervals[i]));//插入到红黑树机制的multimap结构中进行排序
        }
        
        Interval *cur = &((*(in_map.begin())).second);
        for(auto iter = in_map.begin();iter!=in_map.end();iter++){
            if(iter->second.start <=cur->end)//逐个进行合并操作
                cur->end = iter->second.end>cur->end?iter->second.end:cur->end;
            else{
                result.push_back(*cur);//合并失败则说明再没有重叠的区间,直接压入结果数组
                cur = &(iter->second);       
            }
        }
        result.push_back(*cur);//尾部的处理
        return result;
        
    }
};

LeetCode75

颜色排序(LeetCode75:Sort Colors)

给一个数组,其中只有三类颜色:0代表红色,1代表黄色,2代表蓝色。将这数组中的颜色按0,1,2的顺序进行排序,将相同的颜色放在一起。

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

要求:使用一次遍历和常数空间复杂度完成排序

思路:两点法,将0放在首端,2放在尾端,自然1就在中间部分

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int start = 0,end = nums.size()-1;
        int tmp;
        for(int i = 0;i <= end;i++){
            if(nums[i] == 0)//将0放在首端
            {
                tmp = nums[start];
                nums[start] = 0;
                nums[i] = tmp;
                start++;
            }
            
            if(nums[i] == 2){//将2放在尾端
                tmp = nums[end];
                nums[end] = 2;
                nums[i] = tmp;
                end--;
                i--;//和尾部数据进行交换时还要再检测一次
            }
        }
    }
};

LeetCode76

最小匹配窗口(LeetCode76:Minimum Window Substring)

最小包含子串的长度

给出一个字符串s,和一个子字符串t,在s中找到一个最小的子字符串容纳t中所有字符。

Input: S = "ADOBECODEBANC", T = "ABC"
Output: "BANC"

如果没有符合条件的子字符串,返回””

思路:

  • 使用哈希表为匹配字符串t的每一个字符建立索引,并记录出现的次数(该字符要在s的子字符串中出现的次数,其他均设置为0)和所有字符的个数count。
  • 然后遍历s,匹配一个hash表中对应的索引记录的次数减1(减为0时不用匹配),总次数也对应减1。
  • 当总次数减为0时,说明所有要求出现的字符都出现了,然后纪录距离
  • 将该子字符串的前面下班向后移动,所有移出的字符对应索引纪录的次数再加1,当次数大于1时(说明要求子字符串缺少t中的部分字符),count大于0,再移动子字符串的尾部下标寻找新的字符来满足对t的匹配
  • 在如此循环的遍历过程中,记录每一个匹配的子字符串的最小长度和对应的起始下标,最后输出即可

class Solution {
public:
    string minWindow(string s, string t) {
        vector<int> map(128,0);
        int count = 0;
        for(auto c:t) {
            map[c]++;//记录t中每个字符出现的次数
            count++;//记录字符个数
        }
        
        int begin = 0,end = 0,d = INT_MAX;
        int head = 0;
        while (end < s.size()){
            if(map[s[end++]]-->0)
                //说明end位置上的字符在t中有出现
                count--;
            while(count == 0){
                //说明t中的字符均已经出现
                if(end-begin<d){
                    d = end-begin;
                    head = begin;
                }
                if(map[s[begin++]]++==0)
                    //移动前面的下标,但移出一个t中的字符时,增加count,然后继续向后遍历集齐所有的字符,再计算距离
                    count++;
            }
        }
        
        return d==INT_MAX?"":s.substr(head,d);
        
    }
};

LeetCode84

求最大面积子矩阵

给出一个整型数组,每个数字代表一根柱子的高度,这些柱子的宽度均为1,求在这些柱子中找到可以构成的最大面积的矩阵。

LeetCode84.png

思路1:遍历每一根柱子,以柱子高度为矩形的高度,求得左边和右边可以扩展的宽度,然后求面积,遍历结束后访问最大面积。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        if(heights.empty())
            return 0;
        int left = 0,right = 0;
        int maxarea = 0,area = 0;
        int size = heights.size();
        for(int i = 0;i<size;i++){//遍历每一根柱子
            if(i>0){
                if(heights[i] == heights[i-1])
                    continue;
            }
            left = right = i;
            while(left>0){ //向左扩展
                left --;
                if(heights[left]<heights[i]){
                    left++;
                    break;
            
                }
            }
            
            while(right < size-1){//向右扩展
                right++;
                if(heights[right]<heights[i]){
                    right--;
                    break;
                }
            }
            area = heights[i]*(right-left+1);
            if(area>maxarea)
                maxarea = area;
        }
        
        return maxarea;
    }
};

思路2:想法和思路1一致,求左边和右边的可扩展距离

  • 但这里使用栈来实现,栈中记录当前元素h左边第一个高度小于x的元素下标
  • 压入h的下标
  • 之后右边当碰到第一个高度小于h的元素,弹出h的下标
  • 此时左右两边第一个小于h的元素均已经确定。弹出时计算其面积即可。
  • 关键:heights数组中要在尾部压入一个元素0,这样可以弹出栈内所有元素。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        //思路和第一种解法一致,求每一根柱子向左和向右可以扩展的距离,然后求得最大面积
        //我们使用一个栈,栈内的元素记录左边第一个小于当前柱子高度x的元素
        //右边的元素当中,第一个小于x的高度压入栈时,会将x弹出,从而可以得到对于x左右可以扩展的长度
        //从而可以计算其面积
        //关键在于heights中压入一个0元素,这样可以弹出栈内所有的高度元素
        
        heights.push_back(0);
        vector<int> left_vec;
        int h = 0,wid = 0;
        int left = 0;
        int maxarea = 0,area = 0;
        for(int i = 0;i < heights.size();i++){
            while(left_vec.size()>0 && heights[left_vec.back()] > heights[i]){//当栈内有一个元素,且对应的高度小于将压入栈的下标对应的高度
                h = heights[left_vec.back()];
                left_vec.pop_back();//弹出该元素
                left = left_vec.empty()?-1:left_vec.back();//记录左边第一个小于当前高度的元素下标
                wid = i - left -1;
                area = h*wid;//计算以弹出元素高度为基础,左右扩展后的面积
                if(area > maxarea)
                    maxarea = area;
            }
            left_vec.push_back(i);//压入当前元素下标
        }
        return maxarea;
        
    }
};

LeetCode122

股票交易(LeetCode122:Best Time to Buy and Sell Stock II)

OJ链接

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

解答

遍历数组,如果遇到一个更小的价格,那么更新买入的价钱,否则,如果价格大于买入价格,则执行一次计算,判断是否是更大利润:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int buy = INT_MAX,sell,maxprofit = 0;
        for(int i = 0;i < prices.size();i++){
            if(prices[i] < buy) buy = prices[i];
            else if(prices[i] > buy){ 
                sell = prices[i];       
                if(sell - buy > maxprofit)  maxprofit = sell - buy;
            }
        }
        return maxprofit;
    }
};



买卖股票的最佳时机II

OJ链接

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

解答

每次如果遇到一个更小的价格,那么更新买入的价格buy。否则,如果遇到一个更大的价格,那么立即结算,将利润加入到结果中,并且更新买入的价格。如果不立即卖出,当遇到一个更大的价格时再卖出,结果也是相同的,但是这样无法预测往后是否会出现更高的价格,所以立即卖出更方便实现。如果不卖出,当遇到一个更小价格时,会更新买入的价格buy,那么这一笔利润就浪费了

总的来说就是累加所有上升的走势

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int buy = INT_MAX,maxprofit = 0;
        for(int i = 0;i < prices.size();i++){
            if(prices[i] < buy) buy = prices[i];
            else if(prices[i] > buy){
                maxprofit += prices[i] - buy;
                buy = prices[i];
            }
        }
        return maxprofit;
    }
};



买卖股票的最佳时机III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1] 
输出: 0 
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。

解答

1)动态规划

dp(i,k)表示前i天最多完成k(这里“最多”完成2笔,所以k不会大于2)笔交易的最大利润

“最多”完成2笔交易,那么也可以只完成1笔交易,或者不进行交易。同一天可以买进卖出、买进卖出…,但是这样和没进行交易没有区别。总之,dp(i,k)总是有意义的

对于第i天,我们有2种选择,进行交易或者不交易(即卖出或者不卖出):

  • 如果不卖出,那么dp(i,k) = dp(i - 1,k)
  • 如果卖出,那么dp(i,k) = max{prices[i] - prices[j] + dp(j,k - 1)}0 ≤ j ≤ i),也就是下列情况中的最大值:
    • 在第i天最后一次买入:prices[i] - prices[i] + dp(i,k - 1) = dp(i,k - 1)这种情况其实可以省略,因为当第i天不进入买入又卖出时,利润肯定不会小于第i天买进,然后又在第i天卖出,因为同一天买进卖出没有利润,反而浪费了一笔可以进行的交易
    • 在第i - 1天最后一次买入:prices[i] - prices[i - 1] + dp(i - 1,k - 1)
    • 在第i - 2天最后一次买入:prices[i] - prices[i - 2] + dp(i - 2,k - 1)
    • 在第i - 3天最后一次买入:prices[i] - prices[i - 3] + dp(i - 3,k - 1)
    • 在第1天最后一次买入:prices[i] - prices[1] + dp(1,k - 1)
    • 在第0天最后一次买入:prices[i] - prices[0] + dp(0,k - 1)

所以dp(i,k) = max{dp(i - 1,k),max{prices[i] - prices[j] + dp(j,k - 1)}}0 ≤ j < i

  • 时间复杂度:O(k * n^2)(199 / 200 个通过测试用例)
  • 空间复杂度:O(k * n)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() <= 1)  return 0;
        
        int sz = prices.size();
        vector<vector<int>> dp(prices.size(),vector<int>(3,0));
        
        for(int i = 1;i < sz;i++)
            for(int k = 1;k <= 2;k++){
                int _max = 0;//max{prices[i] - prices[j] + dp(j,k - 1)}
                for(int j = 0;j < i;j++)
                    _max = max(_max,prices[i] - prices[j] + dp[j][k - 1]);
                dp[i][k] = max(dp[i - 1][k],_max);
            }
        
        return dp[sz - 1][2];
    }
};

2)时间优化

dp(i,k)表示前i天最多完成k(这里“最多”完成2笔,所以k不会大于2)笔交易的最大利润

对于第i天,我们有2种选择,进行交易或者不交易(即卖出或者不卖出):

  • 如果不卖出,那么dp(i,k) = dp(i - 1,k)
  • 如果卖出,那么dp(i,k) = max{prices[i] - prices[j] + dp(j,k - 1)}0 ≤ j < i

和前面不同的时,我们希望快速求得上面第二种情况的值,即:i天最多完成k笔交易,并且第i天卖出时的最大利润。可以设为local(i,k),有local(i,k) = max{prices[i] - prices[j] + dp(j,k - 1)}0 ≤ j < i)。可以分析最后一次买入的时间

  • 如果最后一次买入是在第i - 1天,那么local(i,k) = prices[i] - prices[i - 1] + dp(i - 1,k - 1)
  • 如果最后一次买入在第i - 1天以前,现在不管最后一次买入是在第i - 1天前的哪一天,我们可以假设第i - 1天前买入后,在第i - 1天卖了又买入,这样计算得到的结果相同。所以local(i,k) = prices[i] - prices[i - 1] + local(i - 1,k)

因此:

  • local(i,k) = prices[i] - prices[i - 1] + max{dp(i - 1,k - 1),local(i - 1,k)}
  • dp(i,k) = max{dp(i - 1,k),local(i,k)}

复杂度分析:

  • 时间复杂度:O(k * n)
  • 空间复杂度:O(k * n)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() <= 1)  return 0;
        
        int sz = prices.size();
        vector<vector<int>> dp(prices.size(),vector<int>(3,0));
        vector<vector<int>> local(prices.size(),vector<int>(3,0));
        
        for(int i = 1;i < sz;i++)
            for(int k = 1;k <= 2;k++){
                local[i][k] = prices[i] - prices[i - 1] + max(dp[i - 1][k - 1],local[i - 1][k]);
                dp[i][k] = max(dp[i - 1][k],local[i][k]);
            }
        
        return dp[sz - 1][2];
    }
};

3)优化空间

通过观察状态转移方程,每个状态只受前一个状态影响,因此可以进行状态压缩,优化空间。要注意的是,local[i][k]可能依赖于dp[i - 1][k - 1]。因此,如果k从小增大,那么当前行的dp值会覆盖前一行的dp值。因此获取到的dp[i - 1][k - 1]会不正确。所以状态压缩后,k要改成从大减小

  • 时间复杂度:O(k * n)(由于这里k=2,所以时间复杂度为O(n))
  • 空间复杂度:O(k)(由于这里k=2,所以空间复杂度为O(1))
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() <= 1)  return 0;

        vector<int> dp(3,0);
        vector<int> local(3,0);
        
        for(int i = 1;i < prices.size();i++)
            for(int k = 2;k > 0;k--){
                local[k] = prices[i] - prices[i - 1] + max(dp[k - 1],local[k]);
                dp[k] = max(dp[k],local[k]);
            }
        
        return dp[2];
    }
};



买卖股票的最佳时机IV

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

输入: [3,2,6,5,0,3], k = 2
输出: 7
解释: 在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

解答

解法和买卖股票的最佳时机III相同。但是要注意一点,k可能很大,当k ≥ 天数/2时,问题实际转化成了买卖股票的最佳时机II。此时累积所有增长的走势即可。否则,可能因为k过大而无法分配O(k)的内存空间

为什么是k ≥ 天数/2
假设天数为n,可以描绘出股价的走势曲线,那么为了获取最高利润,至少需要多少笔交易?走势曲线中有多少个上升的曲线,就需要多少笔交易。那么当股价一天涨一天跌一天涨…,即呈锯齿状时,上升的曲线数量最多,等于下降的曲线数量-1或下降的曲线数量或下降的曲线数量加1。所以是k ≥ 天数/2

  • k ≥ 天数/2
    • 时间复杂度:O(n)
    • 空间复杂度:O(1)
  • k < 天数/2
    • 时间复杂度:O(k * n)
    • 空间复杂度:O(k)
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if(prices.size() <= 1 || k == 0)  return 0;
        
        int max_profit = 0;
        
        if (k >= prices.size() / 2) {
            for (int i = 1; i < prices.size(); ++i) {
                if (prices[i] - prices[i - 1] > 0)
                    max_profit += prices[i] - prices[i - 1];
            }
        }
        else{
            vector<int> dp(k + 1,0);
            vector<int> local(k + 1,0);

            for(int i = 1;i < prices.size();i++)
                for(int m = k;m > 0;m--){
                    local[m] = prices[i] - prices[i - 1] + max(dp[m - 1],local[m]);
                    dp[m] = max(dp[m],local[m]);
                }
            max_profit = dp[k];
        }
        
        return max_profit;
    }
};

LeetCode125

有效回文(LeetCode125:Valid Palindrome)

给一个字符串,忽略大小写并只考虑字母和数字,判断该字符串是否为回文

Input: "A man, a plan, a canal: Panama"
Output: true

Input: "race a car"
Output: false

思路:简单,用栈即可

注意:对字符串的判断及操作,详见幕布:C++基础学习三:字符串,向量,数组。

  1. 判断是否为数字或字符:isalnum(c);
  2. 转换为对应小写字母:tolower(c);
class Solution {
public:
    bool isPalindrome(string s) {
        if(s.empty())
            return true;
        vector<char> vec,re_vec;
        int end = s.size()-1;
        for(int i = 0;i<=end;i++){
            if(isalnum(s[i]))//判断是否为数字和字母
                vec.push_back(tolower(s[i]));//转换为对应小写字母
            if(isalnum(s[end-i]))
                re_vec.push_back(tolower(s[end-i]));
        }
        
        int vec_end = vec.size()-1;
        for(int i = 0;i<= vec_end;i++){
            if(vec[i] != re_vec[i])
                return false;
        }
        
        return true;
    }
};

LeetCode134

汽油站(LeetCode134:Gas station)

给两个整型数组,一个数组代表一个环形路径中的汽油站所拥有的汽油总量,另一个数组表示从当前汽油站到下一个汽油站需要耗费的汽油。求是否有一个汽油站可以作为起点跑完整个环形路径。若不能跑完全程则返回-1

Input: 
gas  = [1,2,3,4,5]
cost = [3,4,5,1,2]

Output: 3


Input: 
gas  = [2,3,4]
cost = [3,4,3]

Output: -1

思路1:利用循环,模拟每一个节点为起点的情况。


class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int left = 0;
        bool success = true;
        int size = gas.size();
        for(int i = 0;i<size;i++){
            success = true;
            if(gas[i]>=cost[i]){//以i为起点的条件
                int j = (i==size-1)?0:i+1;
                left = gas[i] - cost[i];//计算剩余汽油量
                while( j != i ){
                    left += gas[j] - cost[j];//模拟路程过程中的汽油消耗情况
                    if(left < 0){
                        success = false;
                        break;
                    }
                    j = (j==size-1)?0:j+1;//到下一个汽油站
                }
                
                if(success){
                    return i;
                }
            }
        }
        
        return -1;
    }
};

思路2:

非常经典的一道题。可以转换成求最大连续和做,但是有更简单的方法。基于一个数学定理:

如果一个数组的总和非负,那么一定可以找到一个起始位置,从他开始绕数组一圈,累加和一直都是非负的

有了这个定理,判断到底是否存在这样的解非常容易,只需要把全部的油耗情况计算出来看看是否大于等于0即可。

那么如何求开始位置在哪?

注意到这样一个现象:

  1. 假如从位置i开始,i+1,i+2…,一路开过来一路油箱都没有空。说明什么?说明从i到i+1,i+2,…肯定是正积累。
  2. 现在突然发现开往 位置j时油箱空了。这说明什么?说明从位置i开始没法走完全程(废话)。那么,我们要 从位置i+1开始重新尝试吗?不需要!为什么?因为前面已经知道,位置i肯定是正积累,那么,如果从位置i+1开始走更加没法走完全程了,因为没有位置i的正积累了。同理,也不用从i+2,i+3,…开始尝试。所以我们可以放心地从位置j+1开始尝试。

代码:

int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
  int start = 0; // 起始位置
  int remain = 0; // 当前剩余燃料
  int debt = 0; // 前面没能走完的路上欠的债

  for (int i = 0; i < gas.size(); i++) {
    remain += gas[i] - cost[i];
    if (remain < 0) {//油箱空了,以下一个位置为起点开始重新判断
      debt += remain;//跑完前面部分所需要的油耗
      start = i + 1;
      remain = 0;
    }
  }

  return remain + debt >= 0 ? start : -1;//以start为起点时,当剩余积累下的油量可以满足start之前的油耗时,说明可以跑完全程
}

LeetCode149

求在同一条直线上的点的最大个数(LeetCode149:Max Points on a Line)

如题

Input: [[1,1],[2,2],[3,3]]
Output: 3
Explanation:
^
|
|        o
|     o
|  o  
+------------->
0  1  2  3  4


Input: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
Output: 4
Explanation:
^
|
|  o
|     o        o
|        o
|  o        o
+------------------->
0  1  2  3  4  5  6

思路:

  1. 用斜率记录在同一直线上的点
  2. 从第一个点为起点,和其他点构成直线,计算一条直线拥有最多点的个数
  3. 如此循环计算其它的点的最多的点,后面的点不用再和前面的点重复计算斜率
  4. 得到最多点的个数

知识点:

  1. 斜率的表达:使用pair< int , int>
  2. 求最大公约数 :y==0?x:gcd(y,x%y);

代码实现:

/**
 * Definition for a point.
 * struct Point {
 *     int x;
 *     int y;
 *     Point() : x(0), y(0) {}
 *     Point(int a, int b) : x(a), y(b) {}
 * };
 */
class Solution {
public:
    int maxPoints(vector<Point>& points) {
        int res = 0;
        for(int i = 0;i<points.size();i++){
            map<pair<int,int>,int> lines;
            int localmax = 0;
            int samePoints = 1;
            for(int j = i+1;j<points.size();j++){
                if(points[i].x == points[j].x && points[i].y == points[j].y)
                    samePoints++;
                else
                    localmax = max(localmax ,++lines[getSlope(points[i],points[j])]);
            }
            res = max(res,localmax+samePoints);
        }
        return res;
        
    }
    //求斜率
    pair<int,int> getSlope(Point& p1,Point&p2){
        int dx = p2.x - p1.x;
        int dy = p2.y - p1.y;
        
        if(dx == 0)
            return pair<int,int>(0,p1.y);
        if(dy == 0)
            return pair<int,int>(p1.x,0);
        
        int d = gcd(dx,dy);
        return pair<int,int>(dx/d,dy/d);
        
    }
    
    int gcd(int x,int y){
        return y == 0?x:gcd(y,x%y);//求最大公约数
    }
    
};

LeetCode152

求最大乘积子序列(LeetCode152:Maximum Product Subarray)

Input: [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.

Input: [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

思路:动态规划,需要记录两个信息,当前最大乘积和最小乘积(用于负数)。

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int minLocal = nums[0];
        int maxLocal = nums[0];    
        int maxGlobal = maxLocal;
        int tmp1,tmp2;
        for(int i = 1;i<nums.size();i++){
            tmp1 = max(max(maxLocal*nums[i],minLocal*nums[i]),nums[i]);
            tmp2 = min(min(maxLocal*nums[i],minLocal*nums[i]),nums[i]);
            maxLocal = tmp1;
            minLocal = tmp2;
            if(maxLocal > maxGlobal) maxGlobal = maxLocal;
        }     
        return maxGlobal;
    }
}

LeetCode169

求数组中的主元素(LeetCode169:Majority Element)

给出一个数组,求出该数组中出现次数超过数组总数一半的元素。

思路:见剑指offer题面试题39:数组中出现次数超过一半的数字

代码实现


class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int num = nums[0],count = 1;
        for(int i = 1;i<nums.size();i++){
            if(nums[i] == num){
                count++;
            }else{
                if(count == 0)//为0时更换元素
                    num = nums[i];
                else
                    count--;
                
            }
        }
        
        return num;
    }
};

LeetCode171

Excel列号转换(LeetCode171:Excel Sheet Column Number)

给出一个Excel编码,将其转换为对应的列号

    A -> 1
    B -> 2
    C -> 3
    ...
    Z -> 26
    AA -> 27
    AB -> 28 
    ...
Input: "ZY"
Output: 701

思路(滑铁卢。。。):循环求解

class Solution {
public:
    int titleToNumber(string s) {
        int result = 0;
        int cur;
        for(int i=0;i<s.size();i++){
            cur = s[i]-'A'+1;
            result = 26*result+cur;
        }