目录

(带*号的代表复习时仍然做不出来的题目)

JZ4 二维数组中的查找

JZ5 替换空格

JZ6 从尾到头打印链表

JZ7 重建二叉树

JZ8 二叉树的下一个结点(*)

JZ9 用两个栈实现队列

JZ10 斐波那契数列

JZ11 旋转数组的最小数字(#)

JZ12 矩阵中的路径

JZ13 机器人的运动范围(#)

JZ14 剪绳子(#)

JZ15 二进制中1的个数(*)

JZ16 数值的整数次方(*)

JZ19 正则表达式匹配(?)

JZ20 表示数值的字符串(?)

JZ21 调整数组顺序使奇数位于偶数前面(一)

JZ22 链表中倒数最后k个结点

JZ23 链表中环的入口结点

JZ24 反转链表

JZ25 合并两个排序的链表(*)

JZ26 树的子结构(**)

JZ27 二叉树的镜像(*)

JZ28 对称的二叉树

JZ29 顺时针打印矩阵(*)

JZ30 包含min函数的栈

JZ31 栈的压入、弹出序列(*)

JZ32 从上往下打印二叉树

JZ32 从上到下打印二叉树 II

JZ32 - III. 从上到下打印二叉树 III

JZ33 二叉搜索树的后序遍历序列(**)

JZ34 二叉树中和为某一值的路径(二)

JZ82 二叉树中和为某一值的路径(一)

JZ35 复杂链表的复制

JZ36 二叉搜索树与双向链表

JZ38 字符串的排列

JZ39 数组中出现次数超过一半的数字

JZ40 最小的K个数

JZ41 数据流中的中位数

JZ42 连续子数组的最大和

JZ43 整数中1出现的次数(从1到n整数中1出现的次数)

JZ44 数字序列中某一位的数字

JZ45 把数组排成最小的数

JZ46 把数字翻译成字符串(*)

JZ47 礼物的最大价值

JZ48 最长不含重复字符的子字符串

JZ49 丑数(*)

JZ50 第一个只出现一次的字符

JZ51 数组中的逆序对

JZ51 两个链表的公共结点

JZ52 在排序数组中查找数字

JZ53 0~n-1中缺失的数字

JZ54 二叉搜索树的第k大节点(*)

JZ55 二叉树的深度

JZ55-2 平衡二叉树(*)

JZ56 数组中数字出现的次数

JZ57 和为s的两个数字

JZ57 - II. 和为s的连续正数序列

JZ58 -I. 翻转单词顺序

JZ58 - II. 左旋转字符串

JZ59 - I. 滑动窗口的最大值(*)

JZ59 - II. 队列的最大值

JZ60 n个骰子的点数和

JZ61 扑克牌中的顺子

JZ62 圆圈中最后的数字

JZ63  股票的最大利润

JZ67 把字符串转换为整数

JZ68  二叉搜索树的最近公共祖先



JZ4 二维数组中的查找

方法一暴力

方法二

此处看似是用二分的思想,但是又不是

由于往右走变大,往下走也变大,那么最左下和最右上的可以理解为所谓的中间数:

以上图为例,如果右上的数小于target,则往下走,如果大于target,则往左走,故代码如下:

代码如下:

class Solution {
public:bool findNumberIn2DArray(vector<vector<int>>& array, int target) {if(array.size()==0)return false;if(array[0].size()==0)return false;int startRow=0,endRow=array.size()-1;int startCol=0,endCol=array[0].size()-1;//一开始我还写成endCol=array.size()-1;while(startRow<=endRow&&endCol>=startCol){if(array[startRow][endCol]>target){endCol--;}else if(array[startRow][endCol]<target){startRow++;}else return true;            }return false;}
};

JZ5 替换空格

本题主要就是记住string有push_back的用法

class Solution {
public:string replaceSpace(string s) {     //字符数组string array;   //存储结果for(auto &c : s){   //遍历原字符串if(c == ' '){array.push_back('%');array.push_back('2');array.push_back('0');}else{array.push_back(c);}}return array;}
};

JZ6 从尾到头打印链表

解法一 直接遍历

  • 题目很简单,很朴素。我们直接从这个链表的头节点开始进行遍历。然后我们记录下这个数组的每个节点的信息。最后反转一下整个数组,返回即可。

  • 代码如下

    • 需要直接遍历长度为n的链表的所有的结点,时间复杂度为O(n)
    • 需要存储长度为n的链表的所有的结点,空间复杂度为O(n)
class Solution {
public:vector<int> printListFromTailToHead(ListNode* head) {vector<int> ans;// 从头节点开始进行遍历while(head){// 将每个节点的权值放入动态数组里面ans.push_back(head->val);// 指针后移动head=head->next;}// 反转整个数组reverse(ans.begin(),ans.end());return ans;}
};

解法二 递归写法

  • 由于这个题目需要我们从后面向前面开始打印这个数组。所以我们可以对遍历的结点进行一个递归,我们先递归到这个链表的最后面,然后不断向前收集权值。

  • 代码如下

    • 用DFS同样会遍历所有的结点,时间复杂度为O(n)
    • 需要存储长度为n的链表的所有的结点,空间复杂度为O(n)

class Solution {
public:vector<int> reverseList;vector<int> printListFromTailToHead(ListNode* head) {ReverseList(head);return reverseList;}void ReverseList(ListNode* head){if(head==nullptr)return;ReverseList(head->next);reverseList.push_back(head->val);}};

反转链表:

首先准备一个pre结点初始指向nullptr,表示正在反转结点的前一个结点,再准备一个cur,表示当前正在反转的结点,cur初始化为head。
最后在准备一个temp,表示还未反转的第一个结点。

class Solution {
public:vector<int> printListFromTailToHead(ListNode* head) {ListNode *pre=nullptr;ListNode *cur=head;ListNode *tmp=head;while(cur){tmp=tmp->next;//往后移一位cur->next=pre;pre=cur;cur=tmp;}vector<int> ans;while(pre){ans.push_back(pre->val);pre=pre->next;}return ans;}
};

JZ7 重建二叉树(重点)

下面这是复习时直接做出来的版本

/*** Definition for binary tree* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {
public:TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {if(pre.size()==0)return nullptr;TreeNode *root=new TreeNode(pre[0]);int rootIndex;for(int i=0;i<vin.size();i++)if(root->val==vin[i])rootIndex=i;root->left=reConstructBinaryTree(vector<int>(pre.begin()+1,pre.begin()+1+rootIndex),vector<int>(vin.begin(),vin.begin()+rootIndex) );root->right=reConstructBinaryTree(vector<int>(pre.begin()+1+rootIndex,pre.end()),vector<int>(vin.begin()+rootIndex+1,vin.end()) );return root;}
};

以下是第一次做这些题时的思路

思路:

首先知道前序遍历的数组中第一个元素即为root:

int root = pre[0];//树的第一个值为pre的第一个元素

并且此时这个结点即为根节点:

TreeNode *node = new TreeNode(root);

然后找到中序遍历中该元素所处位置。

auto posi = find(vin.begin(), vin.end(), root);

那么接下来就可以通过中序遍历,根节点以左的都是左子树,根节点以右的都是右子树。

那么就可以知道左子树的数组大小是多大了,右子树同理。

注意到题目所给的函数,参数是两个,一个是前序遍历的数组,一个是中序遍历的数组,返回的是根节点。

根节点很好找,也很好返回,只需要

TreeNode *node = new TreeNode(root);

并且在左子树右子树找完之后return node即可。

那我们如果要找最初的根节点的左子树的根节点,只需要传入该左子树的前序和中序遍历即可。

也就是传入:

reConstructBinaryTree(vector<int> (pre.begin() + 1, pre.begin() + 1 + leftSize),vector<int> (vin.begin(), vin.begin() + leftSize));

然后这个函数返回的是传入的两个数组得到的树的根节点,我们让最初的根节点的左节点设置为它即可,也就是赋值:

        node->left = reConstructBinaryTree(vector<int> (pre.begin() + 1, pre.begin() + 1 + leftSize),vector<int> (vin.begin(), vin.begin() + leftSize));

想获取右子树的根节点同理,传入右子树的前序和中序遍历的数组,并且用根节点的right去接受即可:

        node->right = reConstructBinaryTree(vector<int> (pre.begin() + 1 + leftSize, pre.end()),vector<int> (vin.begin() + leftSize + 1, vin.end()));

获取完了左子树和右子树后,返回根结点即可(放在最后才返回,因为这样才能获取根节点的左节点和右节点):

但是有一种情况可以直接返回:

就是当数组大小为1的时候,此时就不需要遍历其左右子树了,可以直接返回

除此之外,当数组大小为0时,即没有子树的情况,则返回nullptr:

if(pre.size() == 0 )
            return nullptr;

完整代码:

class Solution {
public:TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {if(pre.size() == 0 )return nullptr;int root = pre[0];//树的第一个值为pre的第一个元素TreeNode *node = new TreeNode(root);//如果只剩一个节点了,那么可以直接返回if (pre.size() == 1)return node;auto posi = find(vin.begin(), vin.end(), root);//需要注意一点,此处的posi并不是int型,得到是迭代器类型//检测如果出现错误int leftSize = posi - vin.begin();//begin同理也是迭代器类型int rightSize = vin.end() - posi - 1;//递归求解node->left = reConstructBinaryTree(vector<int> (pre.begin() + 1, pre.begin() + 1 + leftSize),vector<int> (vin.begin(), vin.begin() + leftSize));node->right = reConstructBinaryTree(vector<int> (pre.begin() + 1 + leftSize, pre.end()),vector<int> (vin.begin() + leftSize + 1, vin.end()));return node;}

JZ8 二叉树的下一个结点(*)

暴力法:

本题与其他二叉树的题不同在于多了一个可以指向父节点的指针,因此无论给出哪一个结点,我们都可以通过递归找到父节点:

    TreeLinkNode* GetNext(TreeLinkNode* pNode){TreeLinkNode *root = nullptr;TreeLinkNode *tmp = pNode;// 第一步while (tmp) {root = tmp;tmp = tmp->next;}  }

找到父节点之后,然后进行一次中序遍历,依次将结点存入数组即可(要注意到此处可以直接中序遍历)

然后在数组中,找到该点的下一个元素即可:

          for (int i = 0; i < n; ++i) {if (v[i] == pNode && i + 1 != n) {return v[i+1];}}//如果没找到则返回nullreturn nullptr;

中序遍历:

        void pre_order(TreeLinkNode *root, vector<TreeLinkNode*> &v) {if (!root) {return;}pre_order(root->left, v);v.push_back(root);pre_order(root->right, v);}

最优ji仔细观察,可以把中序(DBHEIAFCG)下一结点归为几种类型:
1、有右子树,下一结点是右子树中的最左结点,例如 B,下一结点是 H
2、无右子树,且结点是该结点父结点的左子树,则下一结点是该结点的父结点,例如 H,下一结点是 E
3、无右子树,且结点是该结点父结点的右子树,则一直沿着父结点追朔,直到找到某个结点是其父结点的左子树(本质和第一种是一样的),如果存在这样的结点,那么这个结点的父结点就是我们要找的下一结点。例如 I,下一结点是 A;例如 G,并没有符合情况的结点,所以 G 没有下一结点

对于2、3两种情况都可以统一概括为:从自己开始找,一直找自己的父亲,直到这个父亲是左结点,那么返回这个父亲的父亲。

class Solution {
public:TreeLinkNode* GetNext(TreeLinkNode* pNode){if(pNode==nullptr)return nullptr;if(pNode->right){pNode=pNode->right;while(pNode->left){pNode=pNode->left;}return pNode;}while(pNode->next){if(pNode->next->left==pNode){return pNode->next;}pNode=pNode->next;}return nullptr;}
};

JZ9 用两个栈实现队列

我第一次写的代码是这样:

    void push(int node) {stack1.push(node);}int pop() {while(!stack1.empty()){stack2.push(stack1.top());stack1.pop();}int ret=stack2.top();stack2.pop();return ret;}

但是这样子遗漏了问题。

比如如下例子:["PSH2","PSH3","POP","PSH1","POP","POP"]

当我们push了两次,然后进行第一次pop的时候,会把2和3放入stack2中,此时stack1如下:

3

2

于是stack2:

2

3

然后pop一次,stack2变成:(一旦进行pop,stack1会被清空)

2(消失)

3

此时当我们再次push1时,stack1变成:

1

此时stack2里仍然有数据,如果按照代码里面的,则会将1这个后来的压入stack2中,则变成:

1

3

那么pop出来的顺序就错了。

为防止这种情况,pop的时候,当stack2有数据时则pop,没数据则再将stack1的数压入栈中:

    void push(int node) {stack1.push(node);}int pop() {if(stack2.empty()){while(!stack1.empty()){stack2.push(stack1.top());stack1.pop();}}int ret=stack2.top();stack2.pop();return ret;}

JZ10 斐波那契数列

第一种比较简单的递归:

class Solution {
public:int Fibonacci(int n) {if(n==1||n==2)return 1;return Fibonacci(n-1)+Fibonacci(n-2);}
};

第二种,采用记忆的方法,记录下曾经计算过的值,防止重复计算:

class Solution {
public:int f[50]{0};//初始时全部初始化为0int Fibonacci(int n) {if (n <= 2) return 1;if(f[n]>0)return f[n];//如果大于0说明该值已经计算过,直接返回即可f[n] = (Fibonacci(n-1)+Fibonacci(n-2));//若没计算过则计算一下return f[n];}
};

第三种,采用动态规划

虽然方法二可以解决此题了,但是如果想让空间继续优化,那就用动态规划,优化掉递归栈空间。 方法二是从上往下递归的然后再从下往上回溯的,最后回溯的时候来合并子树从而求得答案。 那么动态规划不同的是,不用递归的过程,直接从子树求得答案。过程是从下往上。

class Solution {
public:int dp[50]{0};int Fibonacci(int n) {dp[1] = 1, dp[2] =1;for (int i = 3 ; i <= n ; i ++) dp[i] = dp[i-1]+dp[i-2];return dp[n];}
};

JZ11 旋转数组的最小数字(#)

这种二分查找难就难在,arr[mid]跟谁比.
我们的目的是:当进行一次比较时,一定能够确定答案在mid的某一侧。一次比较为 arr[mid]跟谁比的问题。
一般的比较原则有:

  • 如果有目标值target,那么直接让arr[mid] 和 target 比较即可。
  • 如果没有目标值,一般可以考虑 端点

这里我们把target 看作是右端点,来进行分析,那就要分析以下三种情况,看是否可以达到上述的目标。

  1. 情况1,arr[mid] > target:4 5 6 1 2 3

    • arr[mid] 为 6, target为右端点 3, arr[mid] > target, 说明[first ... mid] 都是 >= target 的,因为原始数组是非递减,所以可以确定答案为 [mid+1...last]区间,所以 first = mid + 1
  2. 情况2,arr[mid] < target:5 6 1 2 3 4
    • arr[mid] 为 1, target为右端点 4, arr[mid] < target, 说明答案肯定不在[mid+1...last],但是arr[mid] 有可能是答案,所以答案在[first, mid]区间,所以last = mid;
  3. 情况3,arr[mid] == target:
    • 如果是 1 0 1 1 1, arr[mid] = target = 1, 显然答案在左边
    • 如果是 1 1 1 0 1, arr[mid] = target = 1, 显然答案在右边
      所以这种情况,不能确定答案在左边还是右边,那么就让last = last - 1;慢慢缩少区间,同时也不会错过答案。

误区:那我们肯定在想,能不能把左端点看成target, 答案是不能。

原因:
情况1 :1 2 3 4 5 , arr[mid] = 3. target = 1, arr[mid] > target, 答案在mid 的左侧
情况2 :3 4 5 1 2 , arr[mid] = 5, target = 3, arr[mid] > target, 答案却在mid 的右侧
所以不能把左端点当做target

复杂度分析

时间复杂度:二分,所以为O(longN), 但是如果是[1, 1, 1, 1],会退化到O(n)
空间复杂度:没有开辟额外空间,为O(1)

完整代码:

class Solution {
public:int minNumberInRotateArray(vector<int> rotateArray) {if (rotateArray.size() == 0) return 0;int first = 0, last = rotateArray.size() - 1;while (first < last) { // 最后剩下一个元素,即为答案if (rotateArray[first] < rotateArray[last]) { // 提前退出return rotateArray[first];}int mid = first + ((last - first) >> 1);if (rotateArray[mid] > rotateArray[last]) { // 情况1first = mid + 1;}else if (rotateArray[mid] < rotateArray[last]) { //情况2last = mid;}else { // 情况3--last;}}return rotateArray[first];}
};

int mid=first+(last-first)>>1;

int mid = first + ((last - first) >> 1);

注意,这两种写法结果不同,第二种才正确:

JZ12 矩阵中的路径

采用深度优先遍历加回溯的方式

初始时要判断是否越界

若没越界判断是否匹配,不匹配则返回false,匹配则继续对子进行匹配。

要对上下左右都进行判定

注意此处核心思想在于,用四个上下左右的路径进行一个or的判定,核心思想在于,我们可以将其看作一个树,每个结点有四个子树,而每个子树能否走通,就看这个子树的四个子树是否存在一条能走通的路,例如,如果A子树中存在能走通的路,那么A子树就可以走通。

所以用四个路径的结果去做“或”的操作。

对于此处的递归,最后判断是否存在某个路径,都是从终点,从能走通的最远的路径一步步往回判断是否有能走通的路。(此处想象一棵树,如果存在能走通的路,那么一定是从最底部不断往回延伸的。)

class Solution {
public:/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param matrix char字符型vector<vector<>> * @param word string字符串 * @return bool布尔型*/bool dfs(int i,int j,vector<vector<char> >& matrix,string word,int index){if(i>=matrix.size()||i<0||j<0||j>=matrix[0].size()){return false;}if(index!=word.length()-1){if(word[index]==matrix[i][j]){char tmp=matrix[i][j];matrix[i][j]='#';bool res=dfs(i,j-1,matrix,word,index+1)//左||dfs(i,j+1,matrix,word,index+1)//右||dfs(i-1,j,matrix,word,index+1)//上||dfs(i+1,j,matrix,word,index+1);//下matrix[i][j]=tmp;return res;}else return false;}else{if(word[index]==matrix[i][j])return true;else return false;}}bool hasPath(vector<vector<char> >& matrix, string word) {for(int i=0;i<matrix.size();i++){for(int j=0;j<matrix[0].size();j++){if (dfs(i,j,matrix,word,0))return true;}}return false;}
};

为防止走过的路径再次走过,在访问字符前设置一个‘#’字符,

JZ13 机器人的运动范围(#)

比较经典的dfs和bfs的复习:

class Solution {
public:int runnableNum=0;int getSum(int num){int sum=0;while(num){sum+=num%10;num/=10;}return sum;}void dfs(int x,int y,int threshold,int rows,int cols,bool **array){if(x<0||x>=rows||y<0||y>=cols)return;if(array[x][y]==true)return;if(getSum(x)+getSum(y)>threshold)return;if(getSum(x)+getSum(y)<=threshold){array[x][y]=true;runnableNum++;}dfs(x+1,y,threshold,rows,cols,array);//右dfs(x,y+1,threshold,rows,cols,array);//下dfs(x-1,y,threshold,rows,cols,array);//左dfs(x,y-1,threshold,rows,cols,array);//上}int movingCount(int threshold, int rows, int cols) {bool **array;array=new bool*[rows];for(int i=0;i<rows;i++)array[i]=new bool[cols];for(int i=0;i<rows;i++){for(int j=0;j<cols;j++){array[i][j]=false;}}dfs(0,0,threshold,rows,cols,array);return runnableNum;}
};

JZ14 剪绳子(#)

顺带此处补充一点vector的初始化方法:

(142条消息) 【C/C++】Vector的创建与初始化方法_Captain_zw的博客-CSDN博客_c++ vector初始化

有几点需要提前思考明白。

1.绳子必须要剪,不能不剪。

2.总绳子长度为2时,乘积最大为1,长度为3时,乘积最大为2。

3.分出来的绳子长度为1-4时,此时由于该绳子属于分出来的绳子,可以不必对其切割,故其此时最大长度为n。

此处递归的思想很简单。

我们只需要把绳子分成两段即可,所以例如长度为8的绳子只需要考虑1x7,2x6,3x5,4x4的情况。所以我们进行一个循环,从1x7开始遍历,遍历到7x1,然后选取最大的即可。

那么就是提前用一个值存放最大值,然后遍历中保存最大值:

int ret=0;
        for(int i=1;i<n;i++){
            ret=max(ret,i*backTrack(n-i));
        }

而对于长度为7的这个子绳子,我们同理,只需要递归的求其最大长度即可。

代码如下:

class Solution {
public:int backTrack(int n){if(n<=4)return n;int ret=0;for(int i=1;i<n;i++){ret=max(ret,i*backTrack(n-i));}return ret;}int cutRope(int n) {if(n==2)return 1;if(n==3)return 2;return backTrack(n);}
};

还有一点,此处有个优化自己想到了,但实际上是错的:

但是上面这种情况时间复杂度为O(n!),运行会超时,因此对其优化。

注意,在递归函数传入需要被修改的数组时,我们要传入引用:

如果没有这个引用,就只会是单纯的创建一个局部变量然后修改了。

注意!易错点!

此处虽然只给vector初始化了0-7里的元素,但是第8号元素仍然存在,并且默认初始化为0:

所以这个时候传进去的话,array[8]也会满足这样的条件,然后就直接返回了。那么所有的数据传入后都会得到0的结果。

此处为什么开的数组大小是要n+1呢?

是因为初步我们调用这个函数的时候,我们会传入n这个参数

因此初始化为n+1可以保证array[8]也为-1,那么就必定会进行递归。

除此之外,还有另外一个重要的原因:

对于数组来说,array[i]的本意是用来代表这个子绳子的乘积最大为多大。因为是子绳子,所以长度为1-4的时候不需要切割。当i>=5的时候,此时必然要切割,乘积才能最大,因此此时array[i]不仅可以代表这个子绳子乘积最大为多少,也可以代表这个长度的绳子最大为多少

所以array[8]也就代表递归的最后一步,也就是这个绳子为8的最大乘积是多少!

(以下这种理解有局限性,不建议)

另外一种方法,如果不开辟n+1个长度,也可以在这里添加判断条件:

一个条件不够,那就两个。

不过多开辟一个位置就可以防止越界了。

最终完整代码:

class Solution {
public:int backTrack(int n,vector<int> &array){if(n<=4)return n;/*if(n<array.size()){*/if(array[n]!=-1)return array[n];/*}*/int ret=0;for(int i=1;i<n;i++){ret=max(ret,i*backTrack(n-i,array));}/*if(n<array.size()) */array[n]=ret;return ret;}int cutRope(int n) {if(n==2)return 1;if(n==3)return 2;vector<int> array(n,-1);return backTrack(n+1,array);}};

这种方法的时间复杂度为O(n²),为什么呢,建议用下面的动态规划法更好理解。

方法三:

动态规划

方法二可以看作递归版本的动态规划,方法三可以看作是迭代版本的动态规划。

对于数组来说,array[i]的本意是用来代表这个子绳子的乘积最大为多大。因为是子绳子,所以长度为1-4的时候不需要切割。当i>=5的时候,此时必然要切割,乘积才能最大,因此此时array[i]不仅可以代表这个子绳子乘积最大为多少,也可以代表这个长度的绳子最大为多少

因此,如果长度为8的绳子,那么此时f[8]就可以代表长度为8的绳子的乘积最大为多少!!

代码如下:

class Solution {
public:int cutRope(int n) {if(n==2)return 1;if(n==3)return 2;vector<int> array(n+1,-1);for(int i=1;i<=4;i++){array[i]=i;}for(int i=5;i<=n;i++){for(int j=1;j<i;j++){array[i]=max(array[i],j*array[i-j]);}}return array[n];}};

对比一下递归法和迭代法的差别,递归法是这样,我如果要计算绳子8的最大长度,我得先去1x7,2x6……,那么就得先计算绳子7的最大长度,那么就得计算绳子6的计算长度,是一个从高处不断调用底处得到的结果,并且在计算为7的长度的最大过程中,把这个结果记录下来。

而迭代法是从底部一层一层计算,先计算绳子为5的最大长度,然后存储下来,然后再继续计算绳子为6的最大长度。

总的来说,方法一是基础。方法二,方法三都是在方法一的基础上修改的。

Q:接下来,我们就可以开篇的问题了,什么样的题适合用动态规划?
A:一般,动态规划有以下几种分类:

  1. 最值型动态规划,比如求最大,最小值是多少
  2. 计数型动态规划,比如换硬币,有多少种换法
  3. 坐标型动态规划,比如在m*n矩阵求最值型,计数型,一般是二维矩阵
  4. 区间型动态规划,比如在区间中求最值

其实,根据此题的启发,我们可以换种想法,就是什么样的题适合用暴力递归?
显然就是,可能的情况很多,需要枚举所有种情况。只不过动态规划,只记录子结构中最优的解。

JZ15 二进制中1的个数(*)

方法一:暴力方法

分析:题目给一个有符号的整数int,求整数转化成二进制数后,1的个数。

直接根据题目的描述来提出方法一。有2个问题:
问题1: 如何从十进制数转化到二进制数?
问题2:转化为二进制数后,如果判断有1的个数?

方法1:除2取模法。

int val; // input data
int ans = 0;
while (val != 0) {int tmp = val % 2;if (tmp == 1) ++ans;val /= 2;
}

当然这种方法,对于大部分数据是对的,但是对于-2147483648,二进制为1000...000,一共有31个0.因为计算机使用补码存储二进制数据的。对于这个数据,我们的方***输出0,实际上为1.所以这种方法不对。

方法2:二进制移位法

直接将整数看成二进制,然后采用移位的方法。

int val; // input data
int ans = 0;
while (val != 0) {if (val & 1) ++ans;val >>= 1;
}

代码中val & 1表示val 与 0x000...0001(其中有31个0)进行 & 操作。
val >>= 1表示,如果val的二进制是110,则操作之后会变成011,也就是舍去最低位,然后最高位补0.
但是如果val为负数,最高位会补1,所以对于负数还是有点问题。
我们可以转换一下思路,让一个数0x01从右向左与val的每一位进行&操作来判断

int val; // input data
int ans = 0;
int mark = 0x01;
while (mark != 0) {if (mark & val) ++ans;mark <<= 1;
}

这个算法可以解决此题,但是需要运行32次。
时间复杂度:O(32)
空间复杂度:O(1)

方法3:技巧法

对于上一种解法中,无用操作是,如果当前位是0, 还是会做判断,然后一位一位的移动。
如果,给你一种超能力,你一下可以对从右向左的第一位1直接判断,遇到0直接略过,那效率是不是很快。

现考虑二进制数:val :1101000, val-1: 1100111 那么val & (val-1) : 1100000
如果你会了这个操作,是不是这题就很简单了。

public class Solution {public int NumberOf1(int n) {int cnt = 0;// 循环直到n == 0while(n != 0){// 判断 n & 1 是否为0if((n & 1)!=0)cnt++;//将n进行无符号右移n = n>>>1;}return cnt;}
}

n&(n - 1),其预算结果恰为把 n 的二进制位中的最低位的 1 变为 0之后的结果

算法流程:

初始化数量统计变量 res
循环消去最右边的 1 :当 n = 0时跳出。
res += 1 : 统计变量加 1 ;
n &= n - 1 : 消去数字 n 最右边的 1 。

返回统计数量 res.

JZ16 数值的整数次方(*)

暴力法就不说了

递归法:

n为奇数则可以分解为base^(n/2)* base^(n/2) * base

n为偶数则可以分解为base^(n/2)* base^(n/2)

因此可以采用递归的思想,将需要计算的base的值交给子递归去分解计算。

注意若为负数则需要处理一下:

然后注意有一点,判断是否是奇数用这种方法:

递归写法如下:

class Solution {
public:double Power(double base, int exponent) {if(exponent<0){base=1/base;exponent=-exponent;}if(exponent==0)return 1.0;int tmp=exponent/2;double res=Power(base,tmp);if(exponent&1){return res*res*base;}else{return res*res;}}
}

虽然这样子也可以通过,但是可以将其封装一下:

class Solution {
public:double Power(double base, int exponent) {if(exponent<0){base=1/base;exponent=-exponent;}return tmpPower(base,exponent);}double tmpPower(double base,int exponent){if(exponent==0)return 1.0;int tmp=exponent/2;double res=Power(base,tmp);if(exponent&1){return res*res*base;}else{return res*res;}}
};

快速幂法:

​ 所以可以将幂不断右移,若最低位为1则乘进去。

然后权重也需要不断变换,比如110的最低位的权重是1,倒数第二位权重是1*2,倒数第三位权重是1*2*2。所以每经历一次循环权重都要乘一次。

此处将参数改了,也方便书写,下图就是计算b^n次方的过程

class Solution {
public:double Power(double b, int n) {if(n<0){b=1/b;n=-n;}double x=b;//double res=1;while(n){if(n&1)res*=x;x*=x;//原来写错成x*=b;了n>>=1;}return res;}};

这里有一点自己思考的时候想了很久才明白的一点,注意:

需要连乘的东西是

是这个东西,以及后面的x^(2^3)次方

每个循环中需要连乘的东西就是上面那个

观察一下也就是,如果二进制数为1111。

那么需要连乘的是x^(2^1)    x^(2^2)    x^(2^3)次方

这个要如何在循环中不断改变呢?

就是通过不断自乘才行

所以此处才是x*=x

这样才能实现从x^(2^1)  →  x^(2^2)  再变成 → x^(2^3)

JZ19 正则表达式匹配(?)

感觉不是重点

先放着

JZ20 表示数值的字符串(?)

这题又臭又长的

JZ21 调整数组顺序使奇数位于偶数前面(一)

算法思想一:使用辅助数组

解题思路:

创建两个全新的数组,遍历原数组将其奇数和偶数分别存放在两个数组中,最后将两个数组合并(奇数组放在偶数组前面)

方法二:

class Solution {
public:vector<int> exchange(vector<int>& nums){int i = 0, j = nums.size() - 1;while (i < j){while(i < j && (nums[i] & 1) == 1) i++;while(i < j && (nums[j] & 1) == 0) j--;swap(nums[i], nums[j]);}return nums;}
};

JZ22 链表中倒数最后k个结点

算法思想一:快慢指针

解题思路:

第一个指针先移动k步,然后第二个指针再从头开始,这个时候这两个指针同时移动,当第一个指针到链表的末尾的时候,返回第二个指针即可

算法思想二:栈

解题思路:

把原链表的结点全部压栈,然后再把栈中最上面的k个节点出栈,出栈的结点重新串成一个新的链表即可

public class Solution {/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param pHead ListNode类 * @param k int整型 * @return ListNode类*/public ListNode FindKthToTail (ListNode pHead, int k) {// write code hereif (pHead == null || k == 0){return null;}Stack<ListNode> stack = new Stack<>();//链表节点压栈while (pHead != null) {stack.push(pHead);pHead = pHead.next;}// 判断栈的元素是否小于kif (stack.size() < k){return null;}//在出栈串成新的链表ListNode firstNode = stack.pop();while (--k > 0) {// 将出栈的元素重新连接成为链表ListNode temp = stack.pop();temp.next = firstNode;firstNode = temp;}return firstNode;}
}

需要注意时出栈时,每出栈一个结点需要保留之前的结点与当前结点,然后让当前的结点指向之前的结点,这样就可以实现出栈后将链表的顺序恢复为正常了。

JZ23 链表中环的入口结点

##方法一:哈希法

  1. 遍历单链表的每个结点
  2. 如果当前结点地址没有出现在set中,则存入set中
  3. 否则,出现在set中,则当前结点就是环的入口结点
  4. 整个单链表遍历完,若没出现在set中,则不存在环 ###代码
class Solution {
public:ListNode* EntryNodeOfLoop(ListNode* pHead){unordered_set<ListNode*> st;//使用unordered提升速度while (pHead) {if (st.find(pHead) == st.end()) {//这个是用来查找是否存在的方法,如果查找结果为最后一个不存在的则说明不存在st.insert(pHead);pHead = pHead->next;}else {return pHead;}}return nullptr;}
};

##方法二:双指针法 若不用辅助结构set,该怎么做呢?这里画了一张图

  1. 初始化:快指针fast指向头结点, 慢指针slow指向头结点
  2. 让fast一次走两步, slow一次走一步,第一次相遇在C处,停止
  3. 然后让fast指向头结点,slow原地不动,让后fast,slow每次走一步,当再次相遇,就是入口结点。 如上解释:

(为了便于分析,上图所展示的是假设快指针只比慢指针多走了一圈就相遇的情况,

也即快指针所走过的路程为A->B->C->D->B->C,而慢指针所走过的路程为A->B->C)

由快指针是慢指针的两倍速度关系可得,AB=CDB

所以让它们再往前走一段距离,他们会正好在B点相遇,并且此时B点即环的入口结点。

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) :val(x), next(NULL) {}
};
*/
class Solution {
public:ListNode* EntryNodeOfLoop(ListNode* pHead) {if(pHead==nullptr)return nullptr;ListNode* fast=pHead;ListNode* slow=pHead;while(fast!=nullptr&&fast->next!=nullptr){fast=fast->next->next;slow=slow->next;if(fast==slow)break;}if(fast==nullptr||fast->next==nullptr){return nullptr;}fast=pHead;while(fast!=slow){fast=fast->next;slow=slow->next;}return fast;}
};

JZ24 反转链表

方法一:构造链表

如果此类型的题出现在笔试中,如果内存要求不高,可以采用如下方法:
可以先用一个vector将单链表的指针都存起来,然后再构造链表。
此方法简单易懂,代码好些。

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) :val(x), next(NULL) {}
};*/
class Solution {
public:ListNode* ReverseList(ListNode* pHead) {if(!pHead)return nullptr;vector<ListNode*> v;ListNode *head=pHead;while(head){v.push_back(head);head=head->next;}reverse(v.begin(), v.end());head=v[0];ListNode* cur=head;for(int i=1;i<v.size();i++){cur->next=v[i];cur=cur->next;}cur->next=nullptr;return head;}
};

第一次写的时候犯了个错误顺序弄反了:

方法二:经典反转链表法

需要注意的两个点都在注释里面了

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) :val(x), next(NULL) {}
};*/
class Solution {
public:ListNode* ReverseList(ListNode* pHead) {if(!pHead)return nullptr;ListNode* nex,*cur=nullptr,*pre=nullptr;//不要忘记初始化!!nex=pHead;while(nex->next){cur=nex;nex=nex->next;cur->next=pre;pre=cur;}nex->next=cur;//最后一个指针不要忘记往回指return nex;}
};

JZ25 合并两个排序的链表(*)

方法一使用数组

(这里的思路错误,句具体见下方)

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) :val(x), next(NULL) {}
};*/
class Solution {
public:ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {if(pHead1==nullptr&&pHead2==nullptr)return nullptr;if(pHead1==nullptr)return pHead2;if(pHead2==nullptr)return pHead1;ListNode* l1=pHead1;ListNode* l2=pHead2;while(l1->next&&l2->next){if(l1->val<l2->val){while(l1->next->val<l2->val){l1=l1->next;}ListNode *tmp1=l1->next;l1->next=l2;l1=tmp1;}if(l2->val<l1->val){while(l2->next->val<l1->val){l2=l2->next;}ListNode *tmp2=l2->next;l2->next=l1;l2=tmp2;}}if(l1->val<l2->val){l1->next=l2;}else l2->next=l1;if(pHead1->val<pHead2->val)return pHead1;else return pHead2;}
};

代码这样,可以通过

1、3、5

2、4、6

但是无法通过

1、3、5

1、3、5

改动如下:添加等号的判断条件:

/*
struct ListNode {int val;struct ListNode *next;ListNode(int x) :val(x), next(NULL) {}
};*/
class Solution {
public:ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {if(pHead1==nullptr&&pHead2==nullptr)return nullptr;if(pHead1==nullptr)return pHead2;if(pHead2==nullptr)return pHead1;ListNode* l1=pHead1;ListNode* l2=pHead2;while(l1->next&&l2->next){if(l1->val<=l2->val){while(l1->next->val<=l2->val){l1=l1->next;}ListNode *tmp1=l1->next;l1->next=l2;l1=tmp1;}if(l2->val<l1->val){while(l2->next->val<l1->val){l2=l2->next;}ListNode *tmp2=l2->next;l2->next=l1;l2=tmp2;}}if(l1->val<=l2->val){l1->next=l2;}else l2->next=l1;if(pHead1->val<=pHead2->val)return pHead1;else return pHead;}
};

改动后的链接如下:

1       3       5

↓  ↗  ↓  ↗  ↓

1        3       5

但是无法通过:

事实上,我这里这样的思路应该某步错了,接下来无论怎么改动细节都无法得到很好的答案。

在上面的想法中,我的思路是去改动l1或者l2的链表的结点让他们去互相指向。但是这样的思路不好。

换一个思路,我们不改变l1和l2这个链表,我们用一个新的结点按顺序将它们串起来即可。

思路就是,初始时我们让cur指向一个虚空的结点。然后l1和l2分别指向链表的第一个结点,然后我们对比l1和l2的值谁小,我们让cur的next指向那个小的结点(这一步就是连线),并且还要把cur放到那个小的结点(cur=cur->next)。然后我们让l1和l2的链表中,小的那个结点往后移一位(大的不动)

之后循环操作即可。

那么这样的操作会使得要么l1先走出链表末尾,要么是l2。

当l1或者l2走出了链表时,我们就结束循环,然后再让cur指向非空的那个即可。

即:cur->next = pHead1 ? pHead1 : pHead2;

最后返回最初始的虚空头结点的下一个结点:

return vhead->next;

代码如下:

这里有一个很重要的思想就是每次循环只移动一个元素,只在链表中放入一个元素

(像我原来在一次循环中可能会改变多个元素,这样其实不太好操作)

class Solution {
public:ListNode* Merge(ListNode* pHead1, ListNode* pHead2){ListNode *vhead = new ListNode(-1);ListNode *cur = vhead;while (pHead1 && pHead2) {if (pHead1->val <= pHead2->val) {cur->next = pHead1;pHead1 = pHead1->next;}else {cur->next = pHead2;pHead2 = pHead2->next;}cur = cur->next;}cur->next = pHead1 ? pHead1 : pHead2;return vhead->next;}
};

方法二 递归法

看到答案的时候我有点惊讶,没想到这种明显使用迭代方式的代码还可以使用递归。

总结一下思想如下,拿链表

1 →  3 → 5

2 →  4 → 6

举例来说 对于1,1小于2,那么1的下一个结点可能是2,也可能是3,但是总结来说,就是2和3这两个结点组成的后续链表的结点中的头结点。

然后我们刨去1,看35 和246组成的,第一个结点分别是3和2,那么2的下一个结点可能是4可能是3,也就是说2的下一个结点是由35  和46这两个链表组成的。

因此就可以使用递归的思想,代码如下;

public class Solution {public ListNode Merge(ListNode list1,ListNode list2) {// list1 list2为空的情况if(list1 == null || list2 == null){return list1 != null ? list1 : list2;}// 两个链表元素依次对比if(list1.val <= list2.val){// 递归计算 list1.next, list2list1.next = Merge(list1.next, list2);return list1;}else{// 递归计算 list1, list2.nextlist2.next = Merge(list1, list2.next);return list2;} }
}

JZ26 树的子结构(**)

对下面的解题思路总结来说就是:

把所有树的结点先全部先序遍历出来,然后让每一个结点都去判断这个结点对应的树和目标树是否匹配。

把所有树的结点先全部先序遍历出来,然后让每一个结点都去判断这个结点对应的树和目标树是否匹配。

而下面先序遍历的方法就是通过

isContain(A,B)||isSubStructure(A->left,B)||isSubStructure(A->right,B)

这一行代码实现的!

而对于两个结点和其子树判断是否相等也很简单,就是一步步递归,如果B先结束,则说明匹配,如果A先结束,并且B没结束时,说明不匹配。

如果A和B都没结束的话,首先判断当前结点是否相等,如果不等返回false,如果相等则递归判断左右子树是否相等。

class Solution {
public:bool isSubStructure(TreeNode* A, TreeNode* B) {if(A==nullptr||B==nullptr){return false;}return isContain(A,B)||isSubStructure(A->left,B)||isSubStructure(A->right,B);}bool isContain(TreeNode *A,TreeNode *B){if(B==nullptr)return true;if(A==nullptr||A->val!=B->val)return false;return isContain(A->left,B->left)&&isContain(A->right,B->right);}
};

JZ27 二叉树的镜像(*)

思路,先把根节点的左右节点交换,用一个tmp去实现交换。

然后交换完左右节点后,将左节点看作左节点这颗子树的根节点,那么想要得到这颗左子树,就可以递归的去调用这个左子树的根节点,右子树同理。

/*** struct TreeNode {*    int val;*   struct TreeNode *left;* struct TreeNode *right;*    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* };*/
class Solution {
public:/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param pRoot TreeNode类 * @return TreeNode类*/TreeNode* Mirror(TreeNode* pRoot) {if(pRoot){TreeNode* tmp=pRoot->left;pRoot->left=pRoot->right;pRoot->right=tmp;pRoot->left=Mirror(pRoot->left);pRoot->right=Mirror(pRoot->right);}return pRoot;}
};

在此基础上可以进行简化:

    TreeNode* Mirror(TreeNode* pRoot) {if(pRoot){TreeNode* tmp=Mirror(pRoot->left);pRoot->left=Mirror(pRoot->right);pRoot->right=tmp;}return pRoot;}

栈法:

class Solution {
public:/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param pRoot TreeNode类 * @return TreeNode类*/TreeNode* Mirror(TreeNode* pRoot) {if(pRoot){stack<TreeNode*> treeStack;treeStack.push(pRoot);while(!treeStack.empty()){TreeNode *node=treeStack.top();treeStack.pop();//压入栈之前需要进行判断,因为无法把空的node压入栈中if(node->left){treeStack.push(node->left);}if(node->right){treeStack.push(node->right);}TreeNode* tmp=node->left;node->left=node->right;node->right=tmp;}}return pRoot;}

JZ28 对称的二叉树

一开始自己的做法:

class Solution {
public:bool isSymmetrical(TreeNode* pRoot) {if(pRoot==nullptr)return true;return isSame(pRoot);}bool isSame(TreeNode* Node){if(Node){TreeNode* leftNode=Node->left;TreeNode* rightNode=Node->right;if(leftNode==nullptr&&rightNode!=nullptr)return false;if(leftNode!=nullptr&&rightNode==nullptr)return false;if(leftNode==nullptr&&rightNode==nullptr)return true;if(leftNode->val!=rightNode->val)return false;return isSame(leftNode)&&isSame(rightNode);}return false;}

但是这种忽略了这样的情况:即有子树的对称轴不是在父树上面,而是关于子树对称的。

例如这种情况:

注意,此处的对称是指!:

具体的,我们可以设计一个递归函数 check ,传入待检测的两颗子树的头结点 a 和 b(对于本题都传入 root 即可),在单次查询中有如下显而易见的判断子树是否 “对称” 的 Base Case:

  • a 和 b 均为空节点:符合 “对称” 要求;
  • a 和 b 其中一个节点为空,不符合 “对称” 要求;
  • a 和 b 值不相等,不符合 “对称” 要求;

其他情况,我们则要分别检查 a 和 b 的左右节点是否 “对称” ,即递归调用 check(a.left, b.right) 和 check(a.right, b.left)

故代码如下:


class Solution {
public:bool isSymmetrical(TreeNode* pRoot) {if(pRoot==nullptr)return true;return isSame(pRoot->left,pRoot->right);}bool isSame(TreeNode* leftNode,TreeNode* rightNode){if(leftNode==nullptr&&rightNode!=nullptr)return false;if(leftNode!=nullptr&&rightNode==nullptr)return false;if(leftNode==nullptr&&rightNode==nullptr)return true;if(leftNode->val!=rightNode->val)return false;return isSame(leftNode->right,rightNode->left)&&isSame(leftNode->left,rightNode->right);}};

JZ29 顺时针打印矩阵(*)

按圈来打印,一圈一圈打印,打印的时候传入左边界右边界上边界下边界即可,然后再设置一个访问数组,然后进行while循环输出,即可。


上面为第一次做时候的笔记,事实上结果自己真正笔试遇到了之后发现没有那么简单…为了顺时针打印边边,自己设定边界条件的时候很麻烦…

如上所示,会有边界重叠的情况,那么打印的时候就要多加一多减一就比较麻烦,加上本来数组就是从0开始而不是从1开始了,然后还要考虑什么时候是开区间什么时候是闭区间。

于是最方便的方法是这样,遇到边界不好明确的问题的时候统一使用闭区间!这样调试的时候自己也好理解更好弄懂!

注意下面有好几点值得学习的

首先是初始时就标注好left right top bottom这几个变量并为其赋值

而不是在for循环中使用for(int i=0;i<matrix[0].length-1)

除此之外还有一点,下面全部用的都是闭区间,方便自己理解也方便调试

class Solution {public int[] spiralOrder(int[][] matrix) {if(matrix.length == 0) return new int[0];int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;int[] res = new int[(r + 1) * (b + 1)];while(true) {for(int i = l; i <= r; i++) res[x++] = matrix[t][i]; // left to right.if(++t > b) break;//因为遍历完一行都要让其对应的缩小一圈,但是我们需要判断缩小一圈后是否达到了边界,如果达到了边界则break掉。for(int i = t; i <= b; i++) res[x++] = matrix[i][r]; // top to bottom.if(l > --r) break;for(int i = r; i >= l; i--) res[x++] = matrix[b][i]; // right to left.if(t > --b) break;for(int i = b; i >= t; i--) res[x++] = matrix[i][l]; // bottom to top.if(++l > r) break;}return res;}
}

对于res是从前往后自动保存的,所以用res++可以自动实现往后存储以及下表后移

除此之外,有个很重要的边界重叠问题。

如上图所示,打印完了第一行边界的3,下一次打印下一行应该就不能打印3了,要从下一行开始,这怎么办呢?并且当最外层的第一圈循环完成的时候,第二圈的top不能从头开始而是要从下一行开始,这样就有点麻烦。解法如上所示:

注意到在第一行遍历完成时,然后让top++了。并且接下来开始打印的行数是从top开始的,而不是新建一个变量。

JZ30 包含min函数的栈

多开辟一个栈用来保存最小的数即可,每次给普通的数入栈时,给最小的那个数也进行入栈的操作。

关键来了,如果给min栈压入了一个小的数,然后再次压入大的数时,我们可以将该数变为那个小的数。

这么做有两点函数,在于顶部一定是最小的数,另一方面压入大的数的时候我们没有直接pop丢弃而是将其保留,则是为了防止出栈时,最小的数一直都在。

JZ31 栈的压入、弹出序列(*)

本题思路:使用一个辅助栈来模拟是否能由给出的pushV模拟出真实的popV数组。

过程如下:

首先判断pushV首个数据是否与popV的首个数据相同,如果不同则说明此时需要push入栈,popV当前的front元素并不是此时push后立马出栈的,而是在之后的某个时候出栈。所以pushV的指针后移,而popV的指针不移。

然后就继续将pushV中的元素入栈,并同时做对比,如果相同,那么说明该元素在入栈后立马出栈了,然后此时一一对比popV中的元素和辅助栈中的元素,看看popV是不是在这个时候出栈的,如果和辅助栈中的元素相同说明是此时出栈的。

代码如下:

class Solution {
public:bool IsPopOrder(vector<int> pushV,vector<int> popV) {stack<int> st;int j=0;for(int i=0;i<pushV.size();){//并不是每一次循环if(pushV[i]!=popV[j]){st.push(pushV[i]);i++;}else{i++,j++;//此处由于数值相同,省去了进栈再出栈的过程while(!st.empty()&&st.top()==popV[j]){st.pop();j++;}}}//如果能完美匹配,则这个时候辅助栈应该正好为空,所以此处只需要返回是否为空即可return st.empty();}
};

JZ32 从上往下打印二叉树

bfs的模板如下

void bfs() {vis[] = 0;queue<int> pq(start_val);while (!pq.empty()) {int cur = pq.front(); pq.pop();for (遍历cur所有的相邻节点nex) {if (nex节点有效 && vis[nex]==0){vis[nex] = 1;pq.push(nex)}}}
}
/*
struct TreeNode {int val;struct TreeNode *left;struct TreeNode *right;TreeNode(int x) :val(x), left(NULL), right(NULL) {}
};*/
class Solution {
public:vector<int> PrintFromTopToBottom(TreeNode* root) {vector<int> res;if(!root)return res;queue<TreeNode*> q;q.push(root);while(!q.empty()){TreeNode* nowNode=q.front();q.pop();res.push_back(nowNode->val);if(nowNode->left)q.push((nowNode->left));if(nowNode->right)q.push(nowNode->right);}return res;}
};

JZ32 从上到下打印二叉树 II

与上一题不同,本题要求的是每次打印出一整行,所以有所不同

注意加个for循环,以及qSize要提前保存下来,否则运行中会改变。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> res;if(!root)return res;queue<TreeNode*> q;q.push(root);while(!q.empty()){vector<int> level;int qSize=q.size();for(int i=0;i<qSize;i++){TreeNode* nowNode=q.front();q.pop();level.push_back(nowNode->val);if(nowNode->left)q.push((nowNode->left));if(nowNode->right)q.push(nowNode->right);}res.push_back(level);}return res;}
};

JZ32 - III. 从上到下打印二叉树 III

与上面不同在于使用的是之字型

解决方法:

注!下面这部分是自己写的错误代码

原来我看人家解析使用双端队列,我还以为用双端队列来存储,奇数层push_back偶数层push_front什么的,后来发现这样会有问题。如果上一层是逆序存储的话,那么遍历上一层的结点,然后再正序存储时,那么下一层还是逆序的结果。所以不能用这种方法。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> res;if(!root)return res;deque<TreeNode*> q;q.push_back(root);while(!q.empty()){vector<int> level;int qSize=q.size();for(int i=0;i<qSize;i++){TreeNode* nowNode=q.front();q.pop_front();level.push_back(nowNode->val);if(qSize%2==0){if(nowNode->left)q.push_back((nowNode->left));if(nowNode->right)q.push_back(nowNode->right);}else{if(nowNode->left)q.push_front((nowNode->left));if(nowNode->right)q.push_front(nowNode->right);                    }}res.push_back(level);}return res;}};

正确代码,只是单纯的将level里的数组根据需要逆序存储即可。

这样就不会改变队列里的顺序,也不会因为队列里的顺序逆序而影响下一层push 的顺序了。

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> res;if(!root)return res;queue<TreeNode*> q;q.push(root);while(!q.empty()){vector<int> level;int qSize=q.size();for(int i=0;i<qSize;i++){TreeNode* nowNode=q.front();q.pop();level.push_back(nowNode->val);if(nowNode->left)q.push((nowNode->left));if(nowNode->right)q.push(nowNode->right);}if(res.size()%2==0)res.push_back(level);else{reverse(level.begin(), level.end());res.push_back(level);}}return res;}
};

JZ33 二叉搜索树的后序遍历序列(**)

二叉查找树的特点(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;

1、根据二叉搜索树的后续遍历规律:左子树--右子树--根;序列最后一个元素为根节点,左子树的元素值都小于根节点,右子树的元素值都大于根节点。

思路就是,对于给定的数组,根据后序遍历的定义将数组分为根结点和左右子树的结点。

然后根据二叉搜索树,所有的左子树需要满足结点值小于根结点,右子树的值需要大于根节点。

因此从左往右,找到第一个值大于根节点的,则其为右子树的开头,那么它的右边都应该满足其值大于根节点,因此如果找到值小于根节点的则不符合二叉搜索树要求。

class Solution {
public:bool VerifySquenceOfBST(vector<int> sequence) {if(sequence.size()==0)return false;//初始的判断和之后的判断不一样,初始若为空则返回false,但是如果子树的长度小于等于1,则必然满足,返回true(所以此处分了两个函数)return Recursion(sequence);}bool Recursion(vector<int> sequence){if(sequence.size()<=1)return true;int root=sequence[sequence.size()-1];int index=0;while(sequence[index]<root&&index<sequence.size()-2){index++;if(index==sequence.size()){//如果子树中没有大于根节点的值,全是小于根节点的话,就终止循环index--;break;}}for(int i=index+1;i<sequence.size()-1;i++){if(sequence[i]<=root)return false;}if(sequence.size()>3){vector<int> left(sequence.begin(),sequence.begin()+index);vector<int> right(sequence.begin()+index,sequence.end()-1);return Recursion(left)&&Recursion(right);}else{return true;}}
};

JZ34 二叉树中和为某一值的路径(二)

做这一题之前先看看一的版本

JZ82 二叉树中和为某一值的路径(一)

关键在于每次dfs时,都减去目标的targetSum,如果走到某个路的尽头遍历失败,也不担心targetSum,当回溯时,targetSum减去的值会相当于没有。(因为此处的targetSum使用的不是引用也不是全局变量,而是传入的局部变量)

class Solution {
public:/*** * @param root TreeNode类 * @param sum int整型 * @return bool布尔型*/bool hasPathSum(TreeNode* root, int sum) {if(!root)return false;return dfs(root,sum);}bool dfs(TreeNode *root,int targetSum){if(root==nullptr)return false;targetSum-=root->val;if(root->left==nullptr&&root->right==nullptr&&targetSum==0){return true;}return dfs(root->left,targetSum) || dfs(root->right,targetSum);}

那么接下来继续做第二个版本

有了第一个版本之后,对于第一个版本只需要在过程中保存路径,如果回溯则取消路径即可。

存放路径的方式使用vector,每走一步则存一次路径,每退出一次则popback一次路径。

当遇到满足情况的路径时,则将该路径存放进去res中,res是一个存放vector的vector。

class Solution {
public:/*** * @param root TreeNode类 * @param sum int整型 * @return bool布尔型*/using vi=vector<int>;using vvi =vector<vi>;vector<vector<int> > FindPath(TreeNode* root, int sum) {vi path;vvi res;if(!root)return res;dfs(root,sum,path,res);return res;}void dfs(TreeNode *root,int targetSum,vi &path,vvi &res){if(root==nullptr)return;path.push_back(root->val);targetSum-=root->val;if(root->left==nullptr&&root->right==nullptr&&targetSum==0){res.push_back(path);}dfs(root->left,targetSum,path,res);dfs(root->right,targetSum,path,res);path.pop_back();return;}
};

JZ35 复杂链表的复制

刚开始做这一题时我的思路就是创建新的结点,然后为新的结点赋予和所给定List的结点相同的值。

不过遇到了一个问题,一旦将结点往后移动,则找不到初始的结点了。就像下面这样:

/*
struct RandomListNode {int label;struct RandomListNode *next, *random;RandomListNode(int x) :label(x), next(NULL), random(NULL) {}
};
*/
class Solution {
public:RandomListNode* Clone(RandomListNode* pHead) {if(pHead==nullptr)return nullptr; RandomListNode *newListHead=new RandomListNode(0);RandomListNode *newList=newListHead;RandomListNode *head=pHead;while(head){newList->label=head->label;if(head->next){RandomListNode *nextList=new RandomListNode(0);newList->next=nextList;}head=head->next;}head=pHead;return newListHead;}
};

解决的方法是使用哨兵法,使用一个头结点作为空结点。

上面的思路不准确,我犯的错误不是没使用哨兵,而是在于:

又犯了个低级的错误,创建链表不是这样创建的。

这里创建链表后,仅仅是把结点的下一个指向改变了:

正确的做法应该是还需要将当前结点后移!

于是,下面的代码就可以实现拷贝链表了:

/*
struct RandomListNode {int label;struct RandomListNode *next, *random;RandomListNode(int x) :label(x), next(NULL), random(NULL) {}
};
*/
class Solution {
public:RandomListNode* Clone(RandomListNode* pHead) {if(pHead==nullptr)return nullptr; RandomListNode *newListHead=new RandomListNode(0);RandomListNode *newList=newListHead;RandomListNode *head=pHead;while(head){newList->label=head->label;if(head->next){RandomListNode *nextList=new RandomListNode(0);newList->next=nextList;newList=nextList;}head=head->next;}head=pHead;return newListHead;}
};

看了下别人的做法,很妙的思想。

在于构建一个map映射,新建5个结点为A1B1C1D1E1,然后,比如链表为ABCDE,映射分别为A1B1C1D1E1。

然后map(A)就等于A1。

思路就如下:

map(A)即A1的next就等于map(A->next)

这样就可以实现让A1的映射的next应该指向的那个结点等于A的next要指向的那个结点。

random也是同理,

map(A)->random=map(A->random)

故完整代码如下:

(注意!此处使用unordered_map,有概率无法通过!暂时不知道为什么)

/*
struct RandomListNode {int label;struct RandomListNode *next, *random;RandomListNode(int x) :label(x), next(NULL), random(NULL) {}
};
*/
class Solution {
public:RandomListNode* Clone(RandomListNode* pHead) {if(pHead==nullptr)return nullptr; RandomListNode *newListHead=new RandomListNode(0);RandomListNode *newList=newListHead;RandomListNode *head=pHead;unordered_map<RandomListNode*,RandomListNode*> map;while(head){RandomListNode *node=new RandomListNode(head->label);map[head]=node;head=head->next;}for(auto& [key,value]:map){map[key]->next=map[key->next];map[key]->random=map[key->random];}head=pHead;return map[pHead];}
};

方法二:

  1. 此解法参考了大佬的做法, 主要思路是将原链表的结点对应的拷贝节点连在其后, 最后链表变成 原1 -> 拷1 -> 原2 -> 拷2 -> ... -> null 的形式
  2. 然后我们再逐步处理对应的随机指针, 使用双指针, 一个指针指向原链表的节点, 一个指向拷贝链表的节点, 那么就有 拷->random = 原->random->next (random不为空)
  3. 最后再用双指针将两条链表拆分即可, 此算法大大优化了空间复杂度, 十分优秀

JZ36 二叉搜索树与双向链表

我本来的思路是,如下:

简单来说就是观察:

观察可得,10的左边连接的是8,10的右边链接的是12,所以只需要求10的左子树的最右结点即可,然后让他们互相连接,然后递归的去调用即可。

然后如果该节点是叶子结点,则不需要传入。

代码如下,但是有问题。

/*
struct TreeNode {int val;struct TreeNode *left;struct TreeNode *right;TreeNode(int x) :val(x), left(NULL), right(NULL) {}
};*/
class Solution {
public:TreeNode* Convert(TreeNode* pRootOfTree) {if(!pRootOfTree)return nullptr;TreeNode* root=pRootOfTree;while(root->left){root=root->left;}ConvertNode(pRootOfTree);return root;}void ConvertNode(TreeNode* pRootOfTree){if(!pRootOfTree)return;/*if(!pRootOfTree->left)return;if(!pRootOfTree->right)return;*/if(!pRootOfTree->left||!pRootOfTree->right)return;TreeNode* rootLeftTmp=pRootOfTree->left;TreeNode* rootRightTmp=pRootOfTree->right;TreeNode* rootLeft=rootLeftTmp;TreeNode* rootRight=rootRightTmp;bool leftIsLeaf=false;bool rightIsLeaf=false;if(!rootLeftTmp->left&&!rootLeftTmp->right){leftIsLeaf=true;}if(!rootRightTmp->left&&!rootRightTmp->right){rightIsLeaf=true;}while(rootLeft->right){rootLeft=rootLeft->right;}while(rootRight->left){rootRight=rootRight->left;}pRootOfTree->left=rootLeft;pRootOfTree->right=rootRight;rootLeft->right=pRootOfTree;rootRight->left=pRootOfTree;if(leftIsLeaf)ConvertNode(rootLeftTmp);if(rightIsLeaf)ConvertNode(rootRightTmp);return;}
};

问题在于,比如8原来是叶子结点,那么在传入递归函数时,应该就直接return,但是问题在于,在之前的时候修改了8的结点,让它指向了10,这时候传进去后,将无法判断其是根节点。

那么就会出现错误了。

正确做法:

已知将二叉搜索树进行中序遍历可以得到由小到大的顺序排列,因此本题最直接的想法就是进行中序遍历。
将中序遍历的结果用数组存储下来,得到的数组是有从小到大顺序的。最后将数组中的结点依次连接即可。

示意图如下:

/*
struct TreeNode {int val;struct TreeNode *left;struct TreeNode *right;TreeNode(int x) :val(x), left(NULL), right(NULL) {}
};*/
class Solution {
public:vector<TreeNode*> TreeList;//定义一个数组,根据中序遍历来存储结点。void inorder(TreeNode* root){if (!root) return;inorder(root->left);TreeList.push_back(root);inorder(root->right);}TreeNode* Convert(TreeNode* pRootOfTree) {if (!pRootOfTree) return pRootOfTree;inorder(pRootOfTree);for (int i=0;i<TreeList.size()-1;i++){ //根据数组中的顺序将结点连接,注意i的范围。TreeList[i]->right = TreeList[i+1];TreeList[i+1]->left = TreeList[i];}return TreeList[0];//数组的头部存储的是双向链表的第一个结点。}};

JZ38 字符串的排列

如图所示的全排列可以发现,

对于这个排列,我们是固定A不动,然后交换B与C,从而得到"ABC" 和 "ACB"
同理,对于"BAC"、"BCA" 、"CAB"和"CBA"是同样道理

递归三部曲:

  1. 递归函数的功能:dfs(int pos, string s), 表示固定字符串spos下标的字符s[pos]
  2. 递归终止条件:当pos+1 == s.length()的时候,终止,表示对最后一个字符进行固定,也就说明,完成了一次全排列
  3. 下一次递归:dfs(pos+1, s), 很显然,下一次递归就是对字符串的下一个下标进行固定

但是,对于"ABB"来说,就会有重复,如图

所以,我们用set可以进行去重,并且可以达到按字母顺序排序。

class Solution {
public:vector<string> Permutation(string str) {if(str.empty())return {};set<string> res;Perm(0,str,res);return vector<string> (res.begin(),res.end());}void Perm(int pos,string str,set<string> &res){if(pos==str.length()-1){res.insert(str);return;}for(int i=pos;i<str.length();i++){swap(str[pos],str[i]);Perm(pos+1,str,res);swap(str[i],str[pos]);}return;}};

JZ39 数组中出现次数超过一半的数字

哈希表法:

class Solution {
public:int MoreThanHalfNum_Solution(vector<int> numbers) {unordered_map<int,int> arrayMap;for(auto val : numbers)arrayMap[val]++;for(auto val : numbers){if(arrayMap[val]>numbers.size()/2)return val;}return -1;}
};

方法二:排序法

可以先将数组排序,然后可能的众数肯定在数组中间,然后判断一下。

方法三:候选法(最优解)

加入数组中存在众数,那么众数一定大于数组的长度的一半。
思想就是:如果两个数不相等,就消去这两个数,最坏情况下,每次消去一个众数和一个非众数,那么如果存在众数,最后留下的数肯定是众数。

JZ40 最小的K个数

方法一:直接排序

直接排序,然后取前k小数据。

class Solution {
public:vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {vector<int> ret;if (k==0 || k>input.size()) return ret;sort(input.begin(), input.end());return vector<int>({input.begin(), input.begin()+k});  }
};

时间复杂度:O(nlongn)
空间复杂度:O(1)

方法二:

建立一个容量为k的大根堆的优先队列。遍历一遍元素,如果队列大小<k,就直接入队,否则,让当前元素与队顶元素相比,如果队顶元素大,则出队,将当前元素入队

此处使用优先队列,优先队列的特点就是最大的值优先出栈。

lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

class Solution {
public:vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {vector<int> ret;if (k==0 || k > input.size()) return ret;priority_queue<int, vector<int>> pq;for (const int val : input) {if (pq.size() < k) {pq.push(val);}else {if (val < pq.top()) {pq.pop();pq.push(val);}}}while (!pq.empty()) {ret.push_back(pq.top());pq.pop();}return ret;}
};

方法三:堆

取中位数思想就是把数分成两个部分,[0 ... median - 1], [median], [median ... arr.size() - 1],即[中位数的左边,中位数,中位数的右边]

我们可以用两个堆,一个大顶堆一个小顶堆存放中位数两边的数据,大顶堆放左边,小顶堆放右边,我们将数均分的放在左右两个堆中,这样想要获得中位数就只需要获得大顶堆的最大值和小顶堆的最小值即可。

放数时候的思路就是,我们先将数放进左边的大顶堆中,然后取出最大值放在右边的小顶堆中。那么这一步是放数在右边,但是我们也需要放数在左边,这样才能实现平衡。

所以我们当右边的大小大于左边时,我们就从右边取出数来放左边。

class Solution {
public:#define SCD static_cast<double>priority_queue<int> min_q; // 大顶推priority_queue<int, vector<int>, greater<int>> max_q; // 小顶堆void Insert(int num){min_q.push(num); // 试图加入到大顶推// 平衡一个两个堆max_q.push(min_q.top()); min_q.pop();if (min_q.size() < max_q.size()){min_q.push(max_q.top());max_q.pop();}}double GetMedian(){ return min_q.size() > max_q.size() ? SCD(min_q.top()) : SCD(min_q.top() + max_q.top()) / 2;}};

JZ41 数据流中的中位数

一开始看好像以为很简单就跳过了,但事实上好像是有点东西的啊

因为如果每次加入新的元素都重新进行排序是,时间复杂度会很大,要实现一个数据结构方便对其进行维护,这个数据库每次有新的数据进来时,为保证整个数组有序,可以将其插入在合适的地方,使用二分查找。

最简单的是使用链表,查找元素插入位置 O(logN) (二分查找)、向数组某位置插入元素 O(N) (插入位置之后的元素都需要向后移动一位)。

另外一种方法可以使用堆进行优化:

代码如下:

class MedianFinder {
public:/** initialize your data structure here. */priority_queue<int,vector<int>> small;priority_queue<int, vector<int>, greater<int> > big;MedianFinder() {}void addNum(int num) {if(big.size()<small.size()){small.push(num);big.push(small.top());small.pop();}else if(big.size()>=small.size()){big.push(num);small.push(big.top());big.pop();}}double findMedian() {int sSize=small.size();int bSize=big.size();if((sSize+bSize)%2==0)return (big.top()+small.top())*0.5;else return small.top();}
};/*** Your MedianFinder object will be instantiated and called as such:* MedianFinder* obj = new MedianFinder();* obj->addNum(num);* double param_2 = obj->findMedian();*/

JZ42 连续子数组的最大和

##方法一:动态规划 状态定义:dp[i]表示以i结尾的连续子数组的最大和。所以最终要求dp[n-1] 状态转移方程:dp[i] = max(array[i], dp[i-1]+array[i]) 解释:如果当前元素为整数,并且dp[i-1]为负数,那么当然结果就是只选当前元素 技巧:这里为了统一代码的书写,定义dp[i]表示前i个元素的连续子数组的最大和,结尾元素为array[i-1]

class Solution {
public:int maxSubArray(vector<int>& array) {int sz=array.size();vector<int> vec(sz,0);vec[0]=array[0];//初始为0代表加它没有任何效果int res=array[0];//注意此处赋值不能赋值为0!!for(int i=1;i<vec.size();i++){vec[i]=max(array[i],vec[i-1]+array[i]);//注意上面的比较不是vec[i-1]和vec[i-1]+array[i-1]的比较res=max(res,vec[i]);}return res;}
};

JZ43 整数中1出现的次数(从1到n整数中1出现的次数)

题目描述

输入一个整数 n ,求1~n 这 n 个整数的十进制表示中1出现的次数。
例如,1~13 中包含1的数字有1、10、11、12、13 因此共出现6次。

示例

1

2

输入:n = 13

输出:6

这道题简单意思就是:找出小于等于 n 的非负整数中数字 1 出现的个数

初看这道题,无非都会想到一个简单解法:要么从 1 开始遍历到 n 统计 1 出现的次数。可惜时间复杂度过高,基本都会超时。

既然简单的方法不行,看起来也跟动态规划、递归、数据结构等等无关,也排除贪心或者暴力解法,那不妨考虑用笔在纸上画一画,推导出规律

知识点:数学推导
难度:三星

JZ44 数字序列中某一位的数字

JZ45 把数组排成最小的数

题目抽象:给一个包含n个整数的vector,将n个整数组成一个最小的字符串。

方法一:暴力方法

假设n个整数的索引为[0...n-1],如果我们对这n个索引进行全排列,然后再对每次全排列进行组织一下结果,选取最小的即为答案。比如[1, 2, 3] 的全排列为:

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

STL中有全排列的库函数std::next_permutation(first, last)

方法二:贪心,自定义排序

显然方法一出现了太多无关的排列,我们需要一个最小的数,细想一下可知,如果有两个字符串a,b
如果a + b < b + a, 显然我们希望a排在b的前面,因为a排在前面可以使结果更小。于是我们就自定义排序规则,使得vector中字符串都满足如上规则,那么最后的结果肯定是最小的。
算法步骤:

  1. vector<int> 转化为vector<string>
  2. 自定义上述排序规则
  3. 整合结果

对于第二步的排序规则,实现上,可以用仿函数,lambda表达式,函数指针,针对本题的实现分别如下:

仿函数:

struct Com {bool operator() (string a, string b) {return a + b < b + a;}
};
sort(str.begin(), str.end(), Com()); // Com()为临时对象

lambda表达式:

// 1. 匿名lambda表达式
sort(str.begin(), str.end(), [](string a, string b) {return a + b < b + a;
});
// 2.具名lambda表达式
auto lam = [](string a, string b) {return a + b < b + a;};
sort(str.begin(), str.end(), lam);;

函数指针:

bool static com(string a, string b) {return a + b < b + a;
}
//加static的原因:类成员函数有隐藏的this指针,static 可以去this指针
sort(str.begin(), str.end(), com);

最终代码:

class Solution {
public:string PrintMinNumber(vector<int> numbers) {vector<string> strVec;for(auto val : numbers)strVec.push_back(to_string(val));sort(strVec.begin(),strVec.end(),[](string a,string b){return a+b<b+a;});string str;for(auto val: strVec)str+=val;return str;}
};

JZ46 把数字翻译成字符串(*重难点)

此处是第二次看到的比较快理解的解题思路:

比下面的思路要好

代码如下:

记一下

class Solution {
public:int translateNum(int num) {string numStr=to_string(num);int res=solve(numStr);return res;}int solve(string nums) {int dp[nums.length()+1];dp[0]=1,dp[1]=1;if(nums.length()<3){int num=atoi(nums.c_str());if(num>=10&&num<=25)return 2;else return 1;}for(int i=2;i<=nums.length();i++){string tmp=nums.substr(i-2,2);int num=atoi(tmp.c_str());if(num>=10&&num<=25){dp[i]=dp[i-1]+dp[i-2];}else dp[i]=dp[i-1];}return dp[nums.length()];}};

以下是第一次的解题思路

首先先要理解题意是什么意思

11227 当前译码方法数 译码方案 转移方程
1 $dp[0]=1$ 1→a
11 $dp[1]=2$

11→aa

11→k

$i==1$,拼接的数11在范围内,因此$dp[1] =2$
112 $dp[2]=dp[1]+dp[0]=3$

112→al

112→aab

112→kb

$dp[i] = dp[i-1]+dp[i-2]$
1122 $dp[3]=dp[2]+dp[1]=5$

1122→aav

1122→kv

1122→alb

1122→aabb

1122→kbb

$dp[i] = dp[i-1]+dp[i-2]$
11227 $dp[4]=dp[3]=5$

11227→aavg

11227→kvg

11227→albg

1227→aabbg

11227→kbbg

$dp[i]=dp[i-1]$

下面对一些情况进行分析:

初始只有一位,那必定翻译种类为1。

如果是两位,考虑到0是比较特别的位,因为0不参与翻译。

例如:10 或者20 此时num[i]=='0',此时以0结尾,且以1或者2开头,而num[i-1]==‘1’||'2'

此时对应的字母为第10个或者第20个字母 翻译方式只有一种。

如果10或者20不是初始而是中间的某个数,例如223和22310,这两种数字的翻译方式是一样的。即dp[i]==dp[i-2],代码如下:

注意,题目中的样例不会出现非10、20的情况,即不可能出现30,因为此时无法翻译,所以下面问号处无需翻译

        for(int i=1;i<len;i++){if(nums[i] == '0'){if(nums[i-1] =='1'||nums[i-1] == '2'){if(i == 1) dp[i] = 1;//数字串以10或者20开头的情况else dp[i] = dp[i-2];//数字串中存在10或者20的情况下,当前译码数等于后退两步的译码数}else{//?}}

另一种情况,如果这部分不为0,以1开头或者虽然以2开头但是下一位小于6的话(11-19和21-26),则说明此时可以将两个字母一起翻译,也可以两个字母分开翻译。

例如如果作为开头:25,25的翻译方式有两种。

如果是出现在中间:23325,由于25的翻译方式有两种,因此整体的翻译方式有两种,2332和5,以及233和25,2332的翻译方式即dp[i-1],233的翻译方式即为dp[i-2]。因此翻译方式即为dp[i-1]+dp[i-2]。

代码如下:

            else if(nums[i - 1] == '1' || (nums[i - 1] == '2' && nums[i] >= '1' && nums[i] <= '6')){if(i == 1) dp[i] = 2;//数字串开始不是10或者20的情况,dp[1]=2else dp[i] = dp[i-1] + dp[i-2];}

其他情况,即当前数字和前一个数字组成的是大于26的数,例如1235,那么此时35这个东西是只有一种翻译效果的,所以此时1235的翻译结果就等于12的翻译结果。

这种动态规划,就是一步一步从左往右。

方法二:递归

本题也可采用自上而下,从最大的问题开始,逐渐向下求解 ,因此方法二使用递归的方法。

图解+思路分析

任意给定数字串:”11221“

import java.util.*;public class Solution {public int solve (String nums) {return back(nums.toCharArray(), 0);}// 递归函数public int back(char[] nums, int start){//当start走到终点时,证明已经翻译完毕,直接返回1if(start == nums.length){return 1;}    //当字符为0的时候,0没对应的翻译,所以直接返回0 (此路翻译废掉)if(nums[start] == '0')return 0;//每次翻译一个字符int res1 = back(nums,start+1);int res2 = 0;//如果当前字符等于1 或者 当前字符加上下一个字符合起来小于等于26 则可以一次翻译两个字符if((start < nums.length-1) && (nums[start] == '1' || (nums[start] == '2' &&nums[start+1] <= '6'))){res2 = back(nums,start+2);}//返回结果return res1 + res2;}
}

JZ47 礼物的最大价值

回想之前的方法,就是使用递归调用:

递归调用如下:数据小的时候可以通过,但是数据大的时候不行

class Solution {
public:/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param grid int整型vector<vector<>> * @return int整型*/int maxVal;int maxValue(vector<vector<int> >& grid) {// write code heremaxVal=0;track(grid,0,0,0);return maxVal;}void track(vector<vector<int> >& grid,int x,int y,int val){if(x<0||x>=grid.size()||y<0||y>=grid[0].size()){return;}val+=grid[x][y];track(grid,x+1,y,val);track(grid,x,y+1,val);if(x==grid.size()-1&&y==grid[0].size()-1){maxVal=max(maxVal,val);}val-=grid[x][y];return;}
};

第二种方法,动态规划法:

如下:

具体就是从第1行第一列开始,核心公式就是这个:

maxSum[i][j]=max(maxSum[i-1][j],maxSum[i][j-1])+grid[i][j];
看看左和上哪个大,然后就取最大值,再加上当前格子即可。

有细节需要注意,在于使用第一行时需要用到第0行的数据,使用第一列时需要使用第0列的数据,因此初始的时候需要将第0行第0列的数据初始化好:

        int maxSum[grid.size()][grid[0].size()];maxSum[0][0]=grid[0][0];for(int j=1;j<grid[0].size();j++){maxSum[0][j]=maxSum[0][j-1]+grid[0][j];//不要忘记加上之前的值哦!}for(int i=1;i<grid.size();i++){maxSum[i][0]=maxSum[i-1][0]+grid[i][0];}

JZ48 最长不含重复字符的子字符串

本题第二次写的时候差不多算出来了


主要思路:使用动态规划,记录当前字符之前的最长非重复子字符串长度f(i-1),其中i为当前字符的位置。每次遍历当前字符时,分两种情况:

1)若当前字符第一次出现,则最长非重复子字符串长度f(i) = f(i-1)+1。
2)若当前字符不是第一次出现,则首先计算当前字符与它上次出现位置之间的距离d。

  a. 若d大于f(i-1):即说明前一个非重复子字符串中没有包含当前字符,则可以添加当前字符到前一个非重复子字符串中,所以,f(i) = f(i-1)+1。
  b. 若d小于或等于f(i-1):即说明前一个非重复子字符串中已经包含当前字符,则不可以添加当前字符,所以,f(i) = d。(为什么是d而不是f(i-1)呢?因为这个不重复字符的子字符串的末尾一定得是当前的这个i位)

class Solution {
public:int lengthOfLongestSubstring(string s) {//不要忘了这两种情况if(s.length()==0)return 0;if(s.length()==1)return 1;int maxLen=0;int maxSonLen[s.length()];maxSonLen[0]=1;map<char,int> charIndexMap;charIndexMap[s[0]]=0;for(int i=1;i<s.length();i++){if(charIndexMap.find(s[i])==charIndexMap.end()){charIndexMap[s[i]]=i;maxSonLen[i]=maxSonLen[i-1]+1;}else{int dis=i-charIndexMap[s[i]];charIndexMap[s[i]]=i;if(dis>maxSonLen[i-1]){maxSonLen[i]=maxSonLen[i-1]+1;}else{maxSonLen[i]=dis;}}maxLen=max(maxSonLen[i],maxLen);}return maxLen;}
};

JZ49 丑数(*)

解析:

首先从丑数的定义我们知道,一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方可得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:

(1)丑数数组: 1

乘以2的队列:2

乘以3的队列:3

乘以5的队列:5

选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(2)丑数数组:1,2

乘以2的队列:4

乘以3的队列:3,6

乘以5的队列:5,10

选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(3)丑数数组:1,2,3

乘以2的队列:4,6

乘以3的队列:6,9

乘以5的队列:5,10,15

选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(4)丑数数组:1,2,3,4

乘以2的队列:6,8

乘以3的队列:6,9,12

乘以5的队列:5,10,15,20

选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(5)丑数数组:1,2,3,4,5

乘以2的队列:6,8,10,

乘以3的队列:6,9,12,15

乘以5的队列:10,15,20,25

选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列;

但是上面那样做无疑会出现一个问题,就是可以观察到5的队列中总是会多出很多暂时没有用的数。为了解决这个方法,我们不需要每一次都往三个队列里添加丑数乘以相应的数,因为后续的丑数都是由前面的丑数乘以2、3、5可得。那么我们只需要在需要的时候让其用丑数乘以2或3或5即可

所以2、3、5可以使用不同的index,只有当出现由丑数乘以2产生的丑数成为了最小值加入队列时,我们才需要对2产生的丑数的队列添加新的丑数。

因此代码如下:

class Solution {
public:int GetUglyNumber_Solution(int index) {if(index<=6)return index;int uglyNum[index];uglyNum[0]=1;int index2=0,index3=0,index5=0;for(int i=1;i<index;i++){uglyNum[i]=min(min(uglyNum[index2]*2,uglyNum[index3]*3),uglyNum[index5]*5);//采用三个if语句而没有用else可以实现出现重复的数字时index一起往后移动if(uglyNum[i]==uglyNum[index2]*2)index2++;if(uglyNum[i]==uglyNum[index3]*3)index3++;if(uglyNum[i]==uglyNum[index5]*5)index5++;}return uglyNum[index-1];}
};

JZ50 第一个只出现一次的字符

使用哈希表存储,如果再次出现设其值变为false。

JZ51 数组中的逆序对

考虑一下归并排序:

归并排序是需要使用一个辅助数组,然后对辅助数组分成两个部分分别有一个指针,如下所示:

由于1小于下面这个数组的所有,所以即可直接增加4个逆序数。

这样就利用了数组的部分有序性:

6的时候同理:

 本题中可以这样计算逆序数的前提是两个子数组是分别有序的。

方式如上所示,我们先将其分成单独为一块的,然后进行归并排序,在归并排序的过程中就可以顺便计算逆序数的大小。

代码如下:

思路如下,为了防止修改原始数组,可以拷贝一个新的数组,然后再创建以恶搞辅助数组然后传进去递归函数,注意红框里是len-1,因为使用的是左闭右闭区间。

归并归并,在归并之前得先将其分开,下面的区间也是左闭右闭的。

上面在分割的过程中唯独和归并排序不一样的地方就在于多了个返回值,这个返回值就是计算逆序数的。

下面是归并和计算的部分:

mid-i+1的原因如下所示:

总结来看一下,我们只有在下图的红色框中计算了逆序对,其他地方是没有改动过的,和归并排序一模一样。

完整代码如下:

class Solution {
public:int InversePairs(vector<int>& nums) {int len=nums.size();if(len<2)return 0;int copy[len];for(int i=0;i<len;i++){copy[i]=nums[i];}int tmp[len];//注意此处是闭区间 所以传入的是len-1!!!return SplitArray(copy,0,len-1,tmp);}int SplitArray(int nums[],int left,int right,int tmp[]){if(left==right)return 0;int mid=left+(right-left)/2;int leftArrayNum=SplitArray(nums,left,mid,tmp);//左数组中的逆序数(交由子递归去计算)int rightArrayNum=SplitArray(nums,mid+1,right,tmp);int mergeCount=MergeAndCount(nums,left,mid,right,tmp);return leftArrayNum+rightArrayNum+mergeCount;}int MergeAndCount(int nums[],int left,int mid,int right,int tmp[]){for(int i=left;i<=right;i++){tmp[i]=nums[i];}int i=left,j=mid+1;int count=0;for(int k=left;k<=right;k++){if(i==mid+1){//此处自己在做的时候想到可以使用循环一次解决,但是可以考虑一次大循环仅改变一个数。nums[k]=tmp[j];j++;}else if(j==right+1){nums[k]=tmp[i];i++;}else if(tmp[i]<=tmp[j]){nums[k]=tmp[i];i++;}else{nums[k]=tmp[j];j++;count+=mid-i+1;}}return count;}};

其中一个部分,为了对下面这一块有更深的理解

代码还可以改成这样:将count作为成员变量,然后:

JZ51 两个链表的公共结点

判断两个链表是否相交,可以使用哈希集合存储链表节点。

首先遍历链表 \textit{headA}headA,并将链表 \textit{headA}headA 中的每个节点加入哈希集合中。然后遍历链表 \textit{headB}headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:

如果当前节点不在哈希集合中,则继续遍历下一个节点;

如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都是两个链表的公共节点,因此在链表 \textit{headB}headB 中遍历到的第一个在哈希集合中的节点就是两个链表的第一个公共节点,返回该节点。

如果链表 \textit{headB}headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。

class Solution {
public:ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {unordered_set<ListNode *> visited;ListNode *temp = headA;while (temp != nullptr) {visited.insert(temp);temp = temp->next;}temp = headB;while (temp != nullptr) {if (visited.count(temp)) {return temp;}temp = temp->next;}return nullptr;}
};

下面提供双指针方法的正确性证明。考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。

JZ52 在排序数组中查找数字

二分查找到后往左往右延伸即可,然后用right-left+1

不要忘记二分查找怎么写了

二分查找的循环是while(left<=right)

JZ53 0~n-1中缺失的数字

还是2分

JZ54 二叉搜索树的第k大节点(*)

本文解法基于此性质:二叉搜索树的中序遍历为 递增序列

根据以上性质,易得二叉搜索树的 中序遍历倒序 为 递减序列
因此,求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。

class Solution {
public:int res;int kthLargest(TreeNode* root, int k) {dfs(root,k);return res;}void dfs(TreeNode *root,int &num){//此处的num必须带引用!因为每走一个点则都需要减去相应的值if(!root)return;if(num==0)return;dfs(root->right,num);num--;//注意!!!!这里很重要,因为是右根左的顺序,所以一定是先遍历右子树,再得根结点然后去减if(num==0)res=root->val;dfs(root->left,num);}};

中间这个地方很关键:

因为最大的数据是在最右边的,因此在遍历完了所有右边的右边的右边后,才得到第一个最大的结点,然后此时才用k--,因为k代表的是第几大的数

JZ55 二叉树的深度

第一种方法:dfs

树的遍历方式总体分为两类:深度优先搜索(DFS)、广度优先搜索(BFS);

常见的 DFS : 先序遍历、中序遍历、后序遍历;
常见的 BFS : 层序遍历(即按层遍历)。
求树的深度需要遍历树的所有节点,本文将介绍基于 后序遍历(DFS) 和 层序遍历(BFS) 的两种解法。
方法一:后序遍历(DFS)
树的后序遍历 / 深度优先搜索往往利用 递归 或 栈 实现,本文使用递归实现。
关键点: 此树的深度和其左(右)子树的深度之间的关系。显然,此树的深度 等于 左子树的深度 与 右子树的深度 中的 最大值 +1 。

class Solution {
public:int maxDepth(TreeNode* root) {if(root==nullptr)return 0;return max(maxDepth(root->left),maxDepth(root->right))+1;}
};

此处使用层序遍历可以实现,但是有几点需要改改,先来看看传统的层序遍历:

class Solution {
public:vector<int> PrintFromTopToBottom(TreeNode* root) {vector<int> res;if(!root)return res;queue<TreeNode*> q;q.push(root);while(!q.empty()){TreeNode* nowNode=q.front();q.pop();res.push_back(nowNode->val);if(nowNode->left)q.push((nowNode->left));if(nowNode->right)q.push(nowNode->right);}return res;}
};

注意到一点,普通的层序遍历每次只出一个结点。而我们为了计算层的个数,需要每次出一整层的结点,而不是一次出一个,这样就可以计算层了。

代码如下:

class Solution {
public:int maxDepth(TreeNode* root) {if(!root)return 0;queue<TreeNode*> q;q.push(root);int count = 0;while(!q.empty()){int qSize=q.size();//注意在pop的过程中size会发生变化,所以需要一开始记住size的大小for(int i=0;i<qSize;i++){TreeNode* nowNode=q.front();q.pop();if(nowNode->left)q.push(nowNode->left);if(nowNode->right)q.push(nowNode->right);}count++;            }return count;}
};

JZ55-2 平衡二叉树(*)

二叉树的每个节点的左右子树的高度差的绝对值不超过 1,则二叉树是平衡二叉树。根据定义,一棵二叉树是平衡二叉树,当且仅当其所有子树也都是平衡二叉树,因此可以使用递归的方式判断二叉树是不是平衡二叉树,递归的顺序可以是自顶向下或者自底向上。

方法一:自顶向下的递归

定义函数 height,用于计算二叉树中的任意一个节点 p的高度:

有了计算节点高度的函数,即可判断二叉树是否平衡。具体做法类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差是否不超过 1,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。

代码如下:

class Solution {
public:bool isBalanced(TreeNode* root) {if(!root)return true;return (abs(getHeight(root->left)-getHeight(root->right))<=1)&&isBalanced(root->left)&&isBalanced(root->right);}int getHeight(TreeNode* root){if(!root)return 0;return max(getHeight(root->left),getHeight(root->right))+1;}
};

至于本题的时间复杂度不是那么好算和理解,此处介绍一下:

关键在于,每个子树都要求其高度

方法二 自底向上

方法一由于是自顶向下递归,因此对于同一个节点,函数height 会被重复调用,导致时间复杂度较高。

方法一的具体调用过程是计算左子树的高度计算右子树的高度,而且还要判断左子树自己是否平衡,右子树自己是否平衡。在计算高度时,先一步步递归到最底下,然后再往回走,最后才计算出高度。

在判断左子树是否平衡的时候又要计算左子树的两个子树的高度,这样就会导致重复计算

如果使用自底向上的做法,则对于每个节点,函数height 只会被调用一次。

自底向上递归的做法核心思想就是只求一次高度。

我们首先求根结点的高度,要求根节点的高度就去调用左右子树的高度,然后在计算的过程当中,如果发现左右子树的高度中有不平衡的子树,那此时之后我们无需再多计算什么东西了,可以直接让整个程序返回false了。

因为存在一个不平衡的则整个不平衡!

这种做法其实就是在通过求根节点的高度时候需要去计算左右子树的高度,于是乎,在计算左右子树的高度时顺便判断是否平衡,如果不平衡直接返回-1,这样可以使得根节点的高度也是-1,即不平衡。

如果平衡则才返回正常的高度。

class Solution {
public:bool isBalanced(TreeNode* root) {return getHeight(root)>=0;}int getHeight(TreeNode* root){if(!root)return 0;int leftHeight=getHeight(root->left);int rightHeight=getHeight(root->right);if(abs(leftHeight-rightHeight)>1)return -1;if(leftHeight==-1||rightHeight==-1)return -1;return max(leftHeight,rightHeight)+1;}
};

JZ56 数组中数字出现的次数

方法一哈希

方法二排序搜索法:

这个方法也是特别容易想到的,我们首先对数组进行排序,然后遍历数组,因为数组中其他数字都出现两次,只有目标值出现一次,所以则让我们的指针每次跳两步,当发现当前值和前一位不一样的情况时,返回前一位即可,当然我们需要考虑这种情况,当我们的目标值出现在数组最后一位的情况,所以当数组遍历结束后没有返回值,则我们需要返回数组最后一位,下面我们看一下动图解析。

方法三:set去重法

我们依次遍历元素并与 HashSet 内的元素进行比较,如果 HashSet 内没有该元素(说明该元素第一次出现)则存入,若是 HashSet 已经存在该元素(第二次出现),则将其从 HashSet 中去除,并继续遍历下一个元素。最后 HashSet 内剩下的则为我们的目标数

方法五 栈、队列

本题同样可以使用栈或队列,以栈举例:

方法六:位运算

先将本题简化一下,假设只有一个出现次数为1的数,其他数字出现次数全为2

这个方法主要是借助咱们的位运算符 ^ 按位异或,我们先来了解一下这个位运算符。

按位异或(XOR)运算符“^”是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。相同时为0。

任何数和0异或,仍为本身:a⊕0 = a
任何数和本身异或,为0:a⊕a = 0
异或运算满足交换律和结合律:a⊕b⊕a = (a⊕a)⊕b = 0⊕b = b

我们通过上面的例子了解了异或运算,对应位相异时得 1,相同时得 0,那么某个数跟本身异或时,因为对应位都相同所以结果为 0 , 然后异或又满足交换律和结合律。则

class Solution {public int singleNumber(int[] nums) {int num = 0;//异或for (int x : nums) {    num ^= x;}return num;}
}

JZ57 和为s的两个数字

解题思路:
利用 HashMap 可以通过遍历数组找到数字组合,时间和空间复杂度均为 O(N)O(N) ;
注意本题的 numsnums 是 排序数组 ,因此可使用 双指针法 将空间复杂度降低至 O(1)O(1) 。

算法流程:
初始化: 双指针 i, j 分别指向数组 nums 的左右两端 (俗称对撞双指针)。
循环搜索: 当双指针相遇时跳出;
计算和 s = nums[i] + nums[j];
若 s > targets,则指针 j 向左移动,即执行 j = j - 1 ;
若 s < targets,则指针 i 向右移动,即执行 i = i + 1 ;
若 s = targets ,立即返回数组 [nums[i], nums[j]] ;
返回空数组,代表无和为 targettarget 的数字组合。

正确性的证明:

状态 S(i, j)S(i,j) 切换至 S(i + 1, j)S(i+1,j) ,则会消去一行元素,相当于 消去了状态集合 {S(i, i + 1), S(i, i + 2), ..., S(i, j - 2), S(i, j - 1), S(i, j)S(i,i+1),S(i,i+2),...,S(i,j−2),S(i,j−1),S(i,j) } 。(由于双指针都是向中间收缩,因此这些状态之后不可能再遇到)。
由于 numsnums 是排序数组,因此这些 消去的状态 都一定满足 S(i, j) < targetS(i,j)<target ,即这些状态都 不是解 。
结论: 以上分析已证明 “每次指针 i 的移动操作,都不会导致解的丢失” ,即指针 i 的移动操作是 安全的 ;同理,对于指针 j 可得出同样推论;因此,此双指针法是正确的。

class Solution {public int[] twoSum(int[] nums, int target) {int i = 0, j = nums.length - 1;while(i < j) {int s = nums[i] + nums[j];if(s < target) i++;else if(s > target) j--;else return new int[] { nums[i], nums[j] };}return new int[0];}
}

JZ57 - II. 和为s的连续正数序列

方法一:枚举 + 暴力

枚举每个正整数为起点,判断以它为起点的序列和sum是否等于target即可,由于题目要求序列长度至少大于2,所以枚举的上界为target/2

class Solution {
public:vector<vector<int>> findContinuousSequence(int target) {vector<vector<int>> vec;vector<int> res;int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整for (int i = 1; i <= limit; ++i) {for (int j = i;; ++j) {sum += j;if (sum > target) {sum = 0;break;} else if (sum == target) {res.clear();for (int k = i; k <= j; ++k) {res.emplace_back(k);}vec.emplace_back(res);sum = 0;break;}}}return vec;}
};

emplace_back和push_back效果一样

class Solution {
public:vector<vector<int>> findContinuousSequence(int target) {vector<vector<int>> vec;vector<int> res;int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整for (int x = 1; x <= limit; ++x) {long long delta = 1 - 4 * (x - 1ll * x * x - 2 * target);if (delta < 0) {continue;}int delta_sqrt = (int)sqrt(delta + 0.5);if (1ll * delta_sqrt * delta_sqrt == delta && (delta_sqrt - 1) % 2 == 0) {int y = (-1 + delta_sqrt) / 2; // 另一个解(-1-delta_sqrt)/2必然小于0,不用考虑if (x < y) {res.clear();for (int i = x; i <= y; ++i) {res.emplace_back(i);}vec.emplace_back(res);}}}return vec;}
};

方法三:重点!!

双指针法,又叫滑动窗口法

滑动窗口,这里也叫双指针,因为题中要求的是正整数,连续的,并且至少含有两个数。所以我们使用两个指针,一个left指向1,一个right指向2,他们分别表示窗口的左边界和右边界。然后计算窗口内元素的和。

如果窗口内的值大于target,说明窗口大了,left往右移一步。
如果窗口内的值小于target,说明窗口小了,right往右移一步。
如果窗口内的值等于target,说明找到了一组满足条件的序列,把它加入到列表中

class Solution {
public:vector<vector<int>> findContinuousSequence(int target) {vector<vector<int>> vec;vector<int> res;int sum = 0, limit = (target ) / 2; int left=1,right=1;sum=1;//注意这里left是取等while(left<=limit){if(sum<target){right++;sum+=right;}else if(sum>target){sum-=left;left++;}else{res.clear();for(int k=left;k<=right;k++)res.push_back(k);vec.push_back(res);left++;right++;sum=0;for(int k=left;k<=right;k++)sum+=k;}}return vec;}
};

JZ58 -I. 翻转单词顺序

最简单的方法拼接

方法二双指针:

算法解析:
倒序遍历字符串 ss ,记录单词左右索引边界 ii , jj ;
每确定一个单词的边界,则将其添加至单词列表 resres ;
最终,将单词列表拼接为字符串,并返回即可。
复杂度分析:
时间复杂度 O(N)O(N) : 其中 NN 为字符串 ss 的长度,线性遍历字符串。
空间复杂度 O(N)O(N) : 新建的 list(Python) 或 StringBuilder(Java) 中的字符串总长度 \leq N≤N ,占用 O(N)O(N) 大小的额外空间。

JZ58 - II. 左旋转字符串

方法一 字符串切片

方法二 列表遍历拼接

JZ59 - I. 滑动窗口的最大值(*)

滑动窗口如图所示:

在上述滑动窗口形成及移动的过程中,我们注意到元素是从窗口的右侧进入的,然后由于窗口大小是固定的,因此多余的元素是从窗口左侧移除的。 一端进入,另一端移除,这不就是队列的性质吗?所以,该题目可以借助队列来求解。

题目要求是返回每个窗口中的最大值。那么这个如何解决呢?

我们以数组{5, 3, 4, 1},窗口大小k=3来进行说明。这里我们规定窗口右侧边界为right,左侧边界为left,其值为元素下标。

然后,开始遍历nums = {5, 3, 4, 1}。当right指向第一个元素5时,此时队列为空,将第一个元素5入队

△ 元素5入队

继续考察第二个元素3,此时3小于队列末尾的元素5,因此元素3入队,以便看其是否是下一个窗口的最大值。这时队列中元素从队首到队尾是递减的。

△ 元素3入队

接着考察{5, 3, 4, 1}中的第三个元素4,此时4大于队尾元素3,这表明元素3不可能是窗口「5 3 4」中的最大元素,因此需要将其从队列移除。但队首有元素5存在,因此不能将其从队首移除,那怎么办呢?我们可以将其从队尾移除。

对于这种一端既可以有元素入队,又有元素出队的队列,称之为双向队列

队尾元素3出队之后,由于新的队尾元素5大于当前考察元素4,因此元素4入队,以便看其是否是下一个窗口的最大值。

当元素4入队之后,我们发现这时,队列中元素从队首到队尾依旧是递减的。我们把从队首到队尾单调递减或递增的队列称之为单调队列

△ 元素3出队,元素4入队

接着,考察第四个元素1,此时元素1小于队尾元素4,因此元素1入队。

但这时,窗口内有4个元素,而我们这里规定窗口大小是3,因此,需要缩小窗口左边界left。

在缩小窗口左边界left后,意味着元素5已经不再窗口内了,因此,队首元素5需要出队。也就是说,当队首元素在原数组中的下标小于窗口左边界时,队首元素就需要移除队列。

△ 缩小左边界left,队首元素5出队

至此,该题目的求解思路就清晰了,具体如下:

遍历给定数组中的元素,如果队列不为空且当前考察元素大于等于队尾元素,则将队尾元素移除。直到,队列为空或当前考察元素小于新的队尾元素;
当队首元素的下标小于滑动窗口左侧边界left时,表示队首元素已经不再滑动窗口内,因此将其从队首移除。
由于数组下标从0开始,因此当窗口右边界right+1大于等于窗口大小k时,意味着窗口形成。此时,队首元素就是该窗口内的最大值。

class Solution {
public:vector<int> maxInWindows(vector<int>& nums, int size) {vector<int> maxValInWindow;if(size==0)return maxValInWindow;list<int> numsList;//初始化最初的滑动窗口大小for(int i=0;i<size;i++){//如果当前元素大于链表末尾元素,则将链表末尾元素弹出while(!numsList.empty()&&nums[i]>numsList.back()){numsList.pop_back();}numsList.push_back(nums[i]);}maxValInWindow.push_back(numsList.front());int right;for(int left=1;left<nums.size()-size+1;left++){right=left+size-1;//如果当前元素大于链表末尾元素,则将链表末尾元素弹出while(nums[right]>numsList.back()&&!numsList.empty()){numsList.pop_back();}numsList.push_back(nums[right]);//如果此时的队列头是刚才左移出去的那个数据,那么队头就需要pop出去。if(numsList.front()==nums[left-1]){numsList.pop_front();}maxValInWindow.push_back(numsList.front());}return maxValInWindow;}
};

JZ59 - II. 队列的最大值

普通暴力法

class MaxQueue {int q[20000];int begin = 0, end = 0;
public:MaxQueue() {}int max_value() {int ans = -1;for (int i = begin; i != end; ++i)ans = max(ans, q[i]);return ans;}void push_back(int value) {q[end++] = value;}int pop_front() {if (begin == end)return -1;return q[begin++];}
};

本题其实就是整一个单调栈,根本没想象中那么复杂,使用两个队列,一个队列是普通的队列,一个双端队列是使用pop会得到最大值的队列罢了。

思路如下:

正常情况下,如果要进队列,即:push_back函数,那么要保持队列的单调递减,因为如果出现一个值大的,那么前面比它小的值,我们都不再需要了。

例如数字11112,当出现这个2,前面的1都是没有用的了,因为只要1还在队列中,说明2肯定也在队列中,那么此时最大值必定是2,所以前面所有的1都可以丢掉了。

而获取最大值的函数只要调用双端队列的头即可。

而使用pop函数,我们先pop出普通队列中的数,然后我们判断这个和deque中的队首(即最大值是否相同,如果相同说明,这个最大值是在当前队头出队后就应该消失,所以deque的队头也要pop。)

代码如下:

class MaxQueue {queue<int> q;deque<int> d;
public:MaxQueue() {}int max_value() {if (d.empty())return -1;return d.front();}void push_back(int value) {while (!d.empty() && d.back() < value) {d.pop_back();}d.push_back(value);q.push(value);}int pop_front() {if (q.empty())return -1;int ans = q.front();if (ans == d.front()) {d.pop_front();}q.pop();return ans;}
};

JZ60 n个骰子的点数和

解决方法:剑指 Offer 60. n 个骰子的点数(动态规划,清晰图解) - n个骰子的点数 - 力扣(LeetCode) (leetcode-cn.com)

代码如下

class Solution {
public:vector<double> dicesProbability(int n) {vector<double> dp(6,1.0/6);for(int i=2;i<=n;i++){vector<double> tmp(5*i+1,0);for(int j=0;j<dp.size();j++){for(int k=0;k<6;k++){tmp[j+k]+=dp[j]/6.0;}}dp=tmp;}return dp;}
};

JZ61 扑克牌中的顺子

首先,牌不可能有重复,如果有重复,则不可能有顺子。

接下来,在没有大小王的情况下, 如果最大值减去最小值大于5,则不可能有顺子。

如果有大小王,事实上有无大小王不影响结果。

class Solution {
public:bool IsContinuous(vector<int> nums) {set<int> numSet;int min=100,max=01;for(auto val : nums){if(val==0)continue;if(numSet.find(val)==numSet.end()){numSet.insert(val);}else return false;if(min>val)min=val;if(max<val)max=val;}return max-min<5;}
};

JZ62 圆圈中最后的数字

class Solution {public int lastRemaining(int n, int m) {int ans = 0;// 最后一轮剩下2个人,所以从2开始反推for (int i = 2; i <= n; i++) {ans = (ans + m) % i;}return ans;}
}

JZ63  股票的最大利润

本题比较简单,因为有个限制条件是只能买一次股票。所以第i天的利润很简单,我们用dp[i]代表第i天的利润,则dp[i]=max(dp[i-1],price[i]-minPrice).

其中minPrice是前i-1天的最小价格。

(当然本题也可以直接找到左边中的最小值和右边中的最大值,此即为最大利润了,不过动态规划的思想更值得学习。)

JZ65 不用加减乘除做加法

方法一:平均公式法
问题: 此计算必须使用 乘除法 ,因此本方法不可取,直接排除

public int sumNums(int n) {return (1 + n) * n / 2;
}

方法二: 迭代
问题: 循环必须使用 whilewhile 或 forfor ,因此本方法不可取,直接排除。

public int sumNums(int n) {int res = 0;for(int i = 1; i <= n; i++)res += i;return res;
}

方法三: 递归
问题: 终止条件需要使用 ifif ,因此本方法不可取。
思考: 除了 ifif 和 switchswitch 等判断语句外,是否有其他方法可用来终止递归?

class Solution {
public:int sum=0;int sumNums(int n) {if(n==0)return 0;sum+=n;sumNums(n-1);return sum;}
};

使用短路效应:

JZ67 把字符串转换为整数

JZ68.1  二叉搜索树的最近公共祖先

因为是二叉搜索树,所以具有特点,如果说子树小于根节点,那么一定在左边,子树大于根结点一定在右边。

本题给定了两个重要条件:① 树为 二叉搜索树 ,② 树的所有节点的值都是 唯一 的。根据以上条件,可方便地判断 p,qp,q 与 rootroot 的子树关系,即:

若 root.val<p.val ,则 p 在 root 右子树 中;
若 root.val>p.val ,则 p 在 root 左子树 中;
若 root.val=p.val ,则 p 和 root 指向 同一节点 。

注!下面的迭代指的是让最开始的根结点,让其去往它的左边或者右边移动,而不是让我们要找的两个儿子往上移动(因为子树不能向上动,只能父结点往下动。)

class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {while(root != null) {if(root.val < p.val && root.val < q.val) // p,q 都在 root 的右子树中root = root.right; // 遍历至右子节点else if(root.val > p.val && root.val > q.val) // p,q 都在 root 的左子树中root = root.left; // 遍历至左子节点else break;}return root;}
}

如果进行优化:

递归法,其实与上面类似:

class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if(root.val < p.val && root.val < q.val)return lowestCommonAncestor(root.right, p, q);if(root.val > p.val && root.val > q.val)return lowestCommonAncestor(root.left, p, q);return root;}
}

其实意思就是说,除了结点均在左右两边的情况,其他情况都是有祖先结点的情况的。

JZ68.2  二叉树的最近公共祖先

本题与上面不同,难点在于不是二叉搜索树了,而是一个二叉树。

剑指offer刷题记录相关推荐

  1. 剑指offer刷题记录 python3 Java

    剑指offer刷题记录 python3 Java 剑指 Offer 09. 用两个栈实现队列 剑指 Offer 10- I. 斐波那契数列 剑指 Offer 03. 数组中重复的数字 [★]剑指 Of ...

  2. 剑指offer刷题记录(上)

    记录刷题过程,方便自己后期回顾 题目来自<剑指offer>,在牛客上OC,思路大多来自剑指offer,偶尔来自自己的碎碎念,代码自己瞎写的,如果有更优的方法请告诉我,谢谢大佬们 语言:py ...

  3. 剑指Offer 刷题记录

    文章目录 剑指offer题目 01. 二维数组中的查找 02. 替换空格 03. 从尾到头打印链表 04. 重建二叉树 05. 两个堆栈模拟一个队列 06. 旋转数组的最小数字 07. 斐波那契数列 ...

  4. string类函数和牛客网剑指offer刷题记录

    1.strcat char* strcat(char *strDest,const char *strSrc){assert(strDest && strSrc);char *p = ...

  5. 【LeetCode 剑指offer刷题】回溯法与暴力枚举法题6:Number of Islands

    [LeetCode & 剑指offer 刷题笔记]目录(持续更新中...) Number of Islands Given a 2d grid map of '1's (land) and ' ...

  6. 【LeetCode 剑指offer刷题】树题6:28 对称二叉树(101. Symmetric Tree)

    [LeetCode & 剑指offer 刷题笔记]目录(持续更新中...) 101. Symmetric Tree /**  * Definition for a binary tree no ...

  7. 【LeetCode 剑指offer刷题】数组题2:57 有序数组中和为s的两个数(167 Two Sum II - Input array is sorted)...

    [LeetCode & 剑指offer 刷题笔记]目录(持续更新中...) 57 有序数组中和为s的两个数 题目描述 输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是 ...

  8. 【LeetCode 剑指offer刷题】字符串题6:67 把字符串转成整数

    [LeetCode & 剑指offer 刷题笔记]目录(持续更新中...) 67 把字符串转成整数 题目描述 将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数. 数值为0或者字符 ...

  9. 【LeetCode 剑指offer刷题】树题16:Kth Smallest Element in a BST

    [LeetCode & 剑指offer 刷题笔记]目录(持续更新中...) Kth Smallest Element in a BST Given a binary search tree, ...

  10. 【LeetCode 剑指offer刷题】查找与排序题14:Wiggle Sort(系列)

    [LeetCode & 剑指offer 刷题笔记]目录(持续更新中...) Wiggle Sort II Given an unsorted array nums, reorder it su ...

最新文章

  1. 考研C++必刷题(一)
  2. 南开大学20春计算机应用基础,南开大学-2020春学期《计算机应用基础》在线作业.txt.pdf...
  3. ubuntu从命令打开终端
  4. Google 不要 Android 了?新系统 Fuchsia 或将支持 Java
  5. 「总结」 MLEAutoMaton的各种板子总结
  6. Django实战(一)-----用户登录与注册系统2(数据模型、admin后台、路由视图)
  7. Worksheet Crafter Premium Edition for Mac(教学工作表制作工具)
  8. Picasa2图片查找浏览工具
  9. Lizard 的第一篇博客
  10. 0004-1-模型好坏评判标准
  11. upupoo启动不了 mysql_显示桌面快捷方式
  12. 什么是数据挖掘?数据挖掘的目标是什么?
  13. C语言整数转字符串-递归算法
  14. IFS Applications架构
  15. Linux安装ST-Link GDBServer
  16. python积木式编程_TurnipBit—MicroPython开发板:从积木式编程语言开始学作小小创客...
  17. 皮尔森相关性的相似度
  18. Failed to calculate the value of task ‘:CordovaLib:compileDebugJavaWithJavac‘
  19. JAVA计算机毕业设计在线玩具租赁系统Mybatis+源码+数据库+lw文档+系统+调试部署
  20. 【Pr剪辑】Pr下载链接,基础操作,渲染1080视频,视频导出,音频导出,视频变速和合并

热门文章

  1. contextcapture多区块点云_基于ContextCapture倾斜摄影三维建模及其精度分析
  2. 服务器加密机工作原理,服务器密码机
  3. python websocket服务器端_python实现websocket服务器
  4. HL-1208机器清零方法
  5. 查看linux内存和硬盘
  6. 语音识别(html5+nodejs)
  7. 1月15日云栖精选夜读 | 重磅公开!阿里语音识别模型端核心技术,让你“听”见未来...
  8. 条形码简介_条形码基本常识_条形码基本原理
  9. 在线web表单设计器
  10. Web程序设计-客户端表单验证