14. 不修改数组找出重复的数字
给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。
请找出数组中任意一个重复的数,但不能修改输入的数组。
样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。
返回 2 或 3。
思考题:如果只能使用 O(1) 的额外空间,该怎么做呢?
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int l=1,r=nums.size()-1,ans;//[l,mid],[mid+1,r]
while(l<=r){
int mid=(l+r)/2;
int s=0;
for(auto x: nums){
s+= x<=mid&&x>=l;
}
if(s>mid-l+1){
ans=mid;
r=mid-1;
}else {
l=mid+1;
}
}
return ans;
}
};
说明:时间复杂度O(nlogn)(二分O(logn),二分的while循环里面遍历nums,O(n)),空间复杂度O(1)(没格外开新数组)
根据抽屉原理,范围1~n的数放在n+1个坑里,一定有至少两个数一样。依据分治思想。
二分的左边界是1,因为题目说数的范围在1~n。
15. 二维数组中的查找
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。
请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
样例
输入数组:
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
如果输入查找数值为7,则返回true,如果输入查找数值为5,则返回false。
class Solution {
public:
bool searchArray(vector<vector<int>> array, int target) {
if(array.empty()||array[0].empty())return false;
int i=0,j=array[0].size()-1;
while( i<array.size() && j>=0 ){
if(array[i][j]==target)return true;
if(array[i][j]>target) --j;
else ++i;
}
return false;
}
};
说明:注意到矩阵右上角元素的特殊性,每次可以筛掉一行或一列,时间复杂度O(n)。
17. 从尾到头打印链表
输入一个链表的头结点,按照 从尾到头 的顺序返回节点的值。
返回的结果用数组存储。
样例
输入:[2, 3, 5]
返回:[5, 3, 2]
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
vector<int> printListReversingly(ListNode* head) {
vector<int>ans;
while(head){//当head不为空
ans.push_back(head->val);
head=head->next;
}
return vector<int>(ans.rbegin(),ans.rend());//构造一个新的vector,从尾到头遍历ans,用遍历的结果初始化新vector
}
};
18. 重建二叉树
输入一棵二叉树前序遍历和中序遍历的结果,请重建该二叉树。
注意:
二叉树中每个节点的值都互不相同;
输入的前序遍历和中序遍历一定合法;
样例
给定:
前序遍历是:[3, 9, 20, 15, 7]
中序遍历是:[9, 3, 15, 20, 7]
返回:[3, 9, 20, null, null, 15, 7, null, null, null, null] //注:输出采用二叉树的层序遍历
返回的二叉树如下所示:
3
/ \
9 20
/ \
15 7
/**
* 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>preorder,inorder;
map<int,int>hash;//存储元素在中序遍历中的位置(快速找根)
TreeNode* buildTree(vector<int>& _preorder, vector<int>& _inorder) {
preorder=_preorder,inorder=_inorder;
for(int i=0;i<inorder.size();++i) hash[inorder[i]]=i;//inorder各元素的位置
return dfs(0,preorder.size()-1,0,inorder.size()-1);
}
TreeNode* dfs(int pl,int pr,int il,int ir){
//前序遍历左边界,前右,中序遍历左边界,中右,都是闭区间
if(pl>pr)return nullptr;//
auto root=new TreeNode(preorder[pl]);//new一个root,root是前序遍历左边界
int k=hash[root->val];//根在中序遍历中的位置
auto left=dfs(pl+1,pl+k-il,il,k-1);
/*左子树的前左、前右、中左、中右分别是
this的pl+1(pl是根),pl+1+k-il-1,il,k-1*/
auto right=dfs(pl+k-il+1,pr,k+1,ir);
root->left=left,root->right=right;
return root;
}
};
说明:采用分治的思想。
前序遍历preorder是“根左右”,中序遍历inorder是“左根右”。
第1棵树(第1层):preorder的第1个元素是它的根,在inorder中找到这个根的位置,根的左边是它的左子树,根的右边是它的右子树。再依据这个规则分别处理它的左子树、右子树即可。
如何在inorder中快速找到根的位置?开一个map(哈希表)处理一下即可。
dfs的区间容易弄错,在脑海中想象下面这个图就可以了:
preorder: [根][左左左左][右右右右]
inorder: [左左左左][根][右右右右]
19. 二叉树的下一个节点
给定一棵二叉树的其中一个节点,请找出中序遍历序列的下一个节点。
(求二叉树的后继。在二叉搜索树中,某个结点的后继是比它大的第一个数。)
注意:
如果给定的节点是中序遍历序列的最后一个,则返回空节点;
二叉树一定不为空,且给定的节点一定不是空节点;
样例
假定二叉树是:[2, 1, 3, null, null, null, null], 给出的是值等于2的节点。
则应返回值等于3的节点。
解释:该二叉树的结构如下,2的后继节点是3。
2
/ \
1 3
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode *father;
* TreeNode(int x) : val(x), left(NULL), right(NULL), father(NULL) {}
* };
*/
class Solution {
public:
TreeNode* inorderSuccessor(TreeNode* p) {
if(p->right){
p=p->right;
while(p->left)p=p->left;
return p;
}
while(p->father&&p==p->father->right) p=p->father;
return p->father;
}
};
说明:
中序遍历:“左根右”。
分两种情况:1.该结点有右子树,则该结点的后继是它右子树的最左边的结点。2.该结点(this)无右子树,则该结点的后继要沿着它的父亲结点一直往上找,当某个父亲结点是某个结点x的左儿子为止,this结点的后继就是x。
对于一个点求后继,时间复杂度O(h)(h是二叉树的高度,要么向上找要么向下找)
如果要求每个点的后继,时间复杂度O(n),试证明。
20. 用两个栈实现队列
请用栈实现一个队列,支持如下四种操作:
push(x) – 将元素x插到队尾;
pop() – 将队首的元素弹出,并返回该元素;
peek() – 返回队首元素;
empty() – 返回队列是否为空;
注意:
你只能使用栈的标准操作:push to top,peek/pop from top, size 和 is empty;
如果你选择的编程语言没有栈的标准库,你可以使用list或者deque等模拟栈的操作;
输入数据保证合法,例如,在队列为空时,不会进行pop或者peek等操作;
样例
MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek(); // returns 1
queue.pop(); // returns 1
queue.empty(); // returns false
class MyQueue {
public:
/** Initialize your data structure here. */
stack<int>stk,cache;
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
stk.push(x);
}
void COPY(stack<int>& a, stack<int>& b){
while(a.size()){
b.push(a.top());
a.pop();
}
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
COPY(stk,cache);
int res=cache.top();
cache.pop();//注意pop要弹出,peek不用
COPY(cache,stk);
return res;
}
/** Get the front element. */
int peek() {
COPY(stk,cache);
int res=cache.top();
COPY(cache,stk);
return res;
}
/** Returns whether the queue is empty. */
bool empty() {
return stk.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* bool param_4 = obj.empty();
*/
21. 斐波那契数列
输入一个整数 nn ,求斐波那契数列的第 nn 项。
假定从0开始,第0项为0。(nn<=39)
样例
输入整数 n=5
返回 5
class Solution {
public:
int Fibonacci(int n) {
int a=0,b=1;
while(n--){
int c=a+b;
a=b,b=c;
}
return a;
}
};
22. 旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个升序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
数组可能包含重复项。
注意:数组内所含元素非负,若数组大小为0,请返回-1。
样例
输入:nums=[2,2,2,0,1]
输出:0
class Solution {
public:
int findMin(vector<int>& nums) {
int n=nums.size()-1;
if(n<0)return -1;
while(n>=0&&nums[n]==nums[0])--n;//从数组末尾开始,去掉和nums[0]相等的
if(nums[n]>nums[0])return nums[0];
int l=0,r=n,ans;
while(l<=r){//找小于nums[0]的第一个数
int mid=(l+r)/1;
if(nums[mid]<nums[0]){
ans=nums[mid];
r=mid-1;
}else l=mid+1;
}
return ans;
}
};
说明:二分,可画图观察旋转数组的单调性,找小于nums[0]的第一个数。
只有去掉数组末尾和nums[0]相等的元素(数组可能包含重复项)后才可进行二分(满足二分的条件)。
23. 矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。
路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。
如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。
注意:
输入的路径不为空;
所有出现的字符均为大写英文字母;
样例
matrix=
[
["A","B","C","E"],
["S","F","C","S"],
["A","D","E","E"]
]
str="BCCE" , return "true"
str="ASAE" , return "false"
class Solution {
public:
bool hasPath(vector<vector<char>>& matrix, string str) {
for(int i=0;i<matrix.size();++i)
for(int j=0;j<matrix[i].size();++j)
if(dfs(matrix,str,0,i,j)) return true;//枚举起点
return false;
}
bool dfs(vector<vector<char>>& matrix, string &str,int u,int x,int y){
if(matrix[x][y]!=str[u])return false;//此句必须放下一句前面,否则str长度为1的测试点会WA
if(u==str.size()-1)return true;
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
char t=matrix[x][y];//为了回溯
matrix[x][y]='*';//标记走过
for(int i=0;i<4;++i){//枚举方向
int a=x+dx[i],b=y+dy[i];
if(a>=0&&a<matrix.size()&&b>=0&&b<matrix[a].size()){
if(dfs(matrix,str,u+1,a,b)) return true;
}
}
matrix[x][y]=t;//回溯
return false;
}
};
说明:暴力搜索。dfs里的参数:matrix和str是一直要带着跑的,u是步数,x,y是坐标。
24. 机器人的运动范围
地上有一个 mm 行和 nn 列的方格,横纵坐标范围分别是 0∼m−10∼m−1 和 0∼n−10∼n−1。
一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格。
但是不能进入行坐标和列坐标的数位之和大于 kk 的格子。
请问该机器人能够达到多少个格子?
样例1
输入:k=7, m=4, n=5
输出:20
样例2
输入:k=18, m=40, n=40
输出:1484
解释:当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。
注意:
0<=m<=50
0<=n<=50
0<=k<=100
class Solution {
public:
int get_sum(int n){
int s=0;
while(n) s+=n%10,n/=10;
return s;
}
int movingCount(int threshold, int rows, int cols)
{
if(!rows||!cols) return 0;
int X[4]={-1,1,0,0},Y[4]={0,0,-1,1};
bool inq[55][55];
memset(inq,false,sizeof(inq));
int ans=1;
queue<pair<int,int>>q;
q.push({0,0});
inq[0][0]=true;
while(!q.empty()){
auto top=q.front();
q.pop();
int a=top.first,b=top.second;
for(int i=0;i<4;++i){
int newa=a+X[i],newb=b+Y[i];
if(newa>=0&&newa<rows&&newb>=0&&newb<cols&&get_sum(newa)+get_sum(newb)<=threshold&&!inq[newa][newb]){
++ans;
q.push({newa,newb});
inq[newa][newb]=true;
}
}
}
return ans;
}
};
说明:经典BFS。
25. 剪绳子
给你一根长度为 nn 绳子,请把绳子剪成 mm 段(mm、nn 都是整数,2≤n≤582≤n≤58 并且 m≥2m≥2)。
每段的绳子的长度记为k[0]、k[1]、……、k[m]。k[0]k[1] … k[m] 可能的最大乘积是多少?
例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。
样例
输入:8
输出:18
class Solution {
public:
int maxProductAfterCutting(int n) {
if(n<=3)return 1*(n-1);//题目的边界条件n>=2,m>=2
int res=1;
if(n%3==1) res*=4,n-=4;
else if(n%3==2) res*=2,n-=2;
while(n){
n-=3;
res*=3;
}
return res;
}
};
说明:这里有个神奇的结论:要获得最大乘积,需要把N拆分成尽可能多的3+两个2/一个2。也即,如果N%3==1,拆分出两个2,剩下的全是3;如果N%3==2,拆分出一个2,剩下全是3;如果N%3==0,全是3。
证明:N>0, N=n1+n2+...+nk
1.假设 ni >= 5, 3 * (ni - 3) >= ni ? 3*ni - 9 >= ni ? 2*ni >= 9 ? √ 所以最优解里的所有数一定都小于5
2. ni = 4, 4 = 2 * 2 //所以4可以不在
3. 2 * 2 * 2 < 3 * 3 //所以不可能有3个2出现
26. 二进制中1的个数
输入一个32位整数,输出该数二进制表示中1的个数。
注意:
负数在计算机中用其绝对值的补码来表示。
样例1
输入:9
输出:2
解释:9的二进制表示是1001,一共有2个1。
样例2
输入:-2
输出:31
解释:-2在计算机里会被表示成11111111111111111111111111111110, 一共有31个1。
法1°:
class Solution {
public:
int NumberOf1(int n) {
return __builtin_popcount (n);
}
};
说明:__builtin_popcount(n)返回n的二进制表示中1的个数。
法2°:
class Solution {
public:
int NumberOf1(int _n) {
unsigned int n=_n;
int res=0;
while(n) res+=n&1,n>>=1;
return res;
}
};
说明:先把有符号整数转化成无符号整数,这样就可以把一个负数转化成一个正数,转化的效果:n和_n的二进制表示是完全一样的,但值的含义发生了变化,这样一串二进制码,当它是有符号整数时它是一个负数,当它是无符号整数时它是一个很大的数。
补充:负数在计算机中用补码表示
补数的概念:
3对于10的补数是7,3和7互为补数
在二进制里,每个数的补数都是相对于二进制里非常整的一个数来说的,比如100000000000000000000000000000000
1 01 的补数 11111111111111111111111111111111
2 10 的补数 11111111111111111111111111111110
3 11 的补数 11111111111111111111111111111101
负数在计算机中用它绝对值的补码表示
-3 11111111111111111111111111111101 (3的补码)
27. 数值的整数次方
实现函数double Power(double base, int exponent),求base的 exponent次方。
不得使用库函数,同时不需要考虑大数问题。
注意:
不会出现底数和指数同为0的情况
样例1
输入:10 ,2
输出:100
样例2
输入:10 ,-2
输出:0.01
class Solution {
public:
double Power(double base, int exponent) {
double res=1;
for(int i=0;i<abs(exponent);++i) res*=base;
if(exponent<0) res=1/res;
return res;
}
};