显然这是要用二分搜索。不同的是,数字会出现多个。
包含重复数值的二分搜索,重点在于,a[mid]==k的情况:
int first(int* a,int n,int k){
if(a[0]>k||a[n-1]return -1;
int left=0,right=n-1;
while(left+1int mid=left+(right-left)/2;
if(a[mid]else
right=mid;
}
if(a[left]==k)
return left;
if(a[right]==k)
return right;
return -1;
}
int last(int* a,int n,int k){
if(a[0]>k||a[n-1]return -1;
int left=0,right=n-1;
while(left+1int mid=left+(right-left)/2;
if(a[mid]<=k){
left=mid;
}else
right=mid;
}
if(a[right]==k)
return right;
if(a[left]==k)
return left;
return -1;
}
int GetNumberOfK(vector<int> data ,int k) {
int n=data.size();
if(n<=0)
return 0;
int* a=&data[0];
int fir=first(a,n,k);
if(fir==-1)
return 0;
else
return last(a,n,k)-first(a,n,k)+1;
}
其基本思路是,用left和right对区间进行囊括,首先直接否定a[0]>k和a[n-1]
不多说,递归即可。
int TreeDepth(TreeNode* pRoot){
if(pRoot==nullptr)
return 0;
return 1+max(TreeDepth(pRoot->left),TreeDepth(pRoot->right));
}
平衡二叉树的条件:某根节点,它的左右子树都是平衡树,且左右子树深度相差不大于1.
//如果是平衡树,则返回深度;否则返回-1;
int depth(TreeNode* p){
if(p==nullptr)
return 0;
int dep1=depth(p->left),dep2=depth(p->right);
if(dep1!=-1 && dep2!=-1 &&abs(dep1-dep2)<=1){
return max(dep1,dep2)+1;
}else{
return -1;
}
}
bool IsBalanced_Solution(TreeNode* pRoot) {
return depth(pRoot)!=-1;
}
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
答案很巧妙地使用了异或来求解。异或满足交换和结合律,就如同加法和乘法一样,这意味着一列数进行异或,不论这些数排列如何,结果是一样的。
基本公式:
0^x=x x^x=0
假如:一列数仅仅有一个数出现了一次,其他数都出现两次,求这个数。那么所有的数都进行异或(选择0做基底,类似于累加或累乘),最后的值一定是那个数。
例如:a^b^c^b^a=a^a^b^b^c=0^0^c=c
这里做了升级版,因为数组中有两个这样的数。所以最后的异或结果,是这两个数的异或。所以要找到某个条件,对这两个数进行区分。
因为这两个数是不同的,一定会有一位不同。那么找到这个位,把原数组按照这个位进行分组,那么一定可以把这两个数分入不同的组内。
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
int x=0;
for(int i=0;iint b=1;
while((b&x)==0)//位运算优先级仅仅高于逻辑/条件/赋值/逗号 所以一般都要加括号
b<<=1;
int x1=0;
for(int i=0;iif(data[i]&b)
x1^=data[i];
*num1=x1;
*num2=x^x1;
}
//右边的>单目的>四则》移位》比较》位运算》逻辑条件》赋值
首先用一个x对所有数进行异或,最后x=num1^num2;从右向左找到x第一个为1的位,这表示该位上,num1和num2是不同的。然后用x0对所有该位上是1的数进行异或,那么x0最终一定是num1或num2中的某个数。
tip: 常用位操作
取位操作 x&(1<
题1:输入一个递增排序的数组和一个数字S,在数组中查找两个数,使他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
如果采用两层遍历的方法,需要O(N^2)的时间,显然不是最好的。
例如 2 4 5 6 7 8 9 15 20中选取两数字使得和为15.
这里选用了一个策略:
使用left和right表征这两个数。
首先选取最小的2,与最大的20.两者和大于15
此时如果想要减少它们的和,要么使得left左移动,要么使right左移。显然,只能让right左移,此时两数字为2+15>15:所以还需要让right左移。
现在两数:2+9<15.为了让这两个数的和加大,要么使得left右移,要么使right右移。但是right就是左移过来的,所以只能让left右移。
所以整个过程是:
if A[left]+A[right]
vector<int> FindNumbersWithSum(vector<int> array,int sum) {
vector<int> v;
if(array.empty())
return v;
int left=0,right=array.size()-1;
while(left<=right){
if(array[left]+array[right]==sum){
v.push_back(array[left]);
v.push_back(array[right]);
return v;
}
if(array[left]+array[right]else
right--;
}
return v;
}
题2:从[1,2,3,4…]中找出所有和为S的连续正数序列。
规则和上题不同:初始化left和right为[1,2];如果以left和right做边界的区间,数总和小于S的话,就让right右移;反之让left右移。
上题中,初始时left和right分别在数组的首尾端
本题中,初始时left和right分别是最前端的两个数,如果和不够,就扩右边界;如果超出,则缩左边界。
注意本题是找到所有的子区间。
vector<vector<int> > FindContinuousSequence(int sum) {
vector<vector<int> > v;
if(sum<3)
return v;
//初始的情况:只有[1 2]两个数,和为3
int min=1,max=2,s=3;
while(min//最后的情况,肯定是序列长度越来越短
if(s1;
s+=max;
}else if(s>sum){
s-=min;
min+=1;
}else{
vector<int> v1;
for(int i=min;i<=max;++i)
v1.push_back(i);
v.push_back(v1);
//重点:当找到一段序列后,如何寻找下一段?
//答案:右边界右移。这样才会纳入新数据进入区间。
max++;
s+=max;
}
}
return v;
}
题目1:翻转句子中的单词,但单词间顺序不变。单词之间用空格划分。
思路:每个单词分别反转;然后反转整个句子。
要点:切分每个单词。如果依照空格来划分单词的话,注意最后的单词,结尾是没有空格的。
利用STL可以比较容易实现。注意string类的函数,对下标的兼容性很好。
string ReverseSentence(string str) {
if(str.empty())
return str;
int n=str.length();
int i=0;
while(1){
int j=str.find(" ",i);
if(j==-1){
reverse(str.begin()+i,str.end());
break;
}else{
reverse(str.begin()+i,str.begin()+j);
i=j+1;
}
}
reverse(str.begin(),str.end());
return str;
}
题目2:汇编语言中有一种移位指令叫做循环左移(ROL),现在有一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。
思路:前后两段分别反转;然后整个序列反转。(注意这个顺序不要反!)
string LeftRotateString(string str, int n) {
if(str.empty()||n>str.length())
return str;
reverse(str.begin(),str.begin()+n);
reverse(str.begin()+n,str.end());
reverse(str.begin(),str.end());
return str;
}
求n个骰子扔在地上,点数总和为K的情况有多少种。每个骰子视为不同的。
非常不错的动态规划题。
先尝试用递归的思想,推导出状态转移方程:
F(n,K)=F(n-1,K-1)+F(n-2,K-2)+…F(n-6,K-6)
动态规划一般有若干个步骤n,推导的是F(n)和F(n-1)的关系。在第n步时,选择分支是有限的,选择分支直接影响目标结果。
假设当前有i个骰子,需要凑总和j。那么比较F(i)和F(i-1),所做的唯一改变,就是加入了第i个骰子。第i个骰子的点数可以是1到6,假设点数是3,那么只需i-1个骰子,去凑j-3就可以了。
那么:
F(i,j)=F(i-1,j-1)+F(i-1,j-2)…+F(i-1,j-6)
从i到i-1的状态转移,右边有若干个子项,分别是i步的选择分支。
程序实现的时候,一般是定义二维数组。一般来说,数组的第一行和第一列都要单独空出来(不一定都是0!)以便于后续迭代。
int func(int n,int k){
//注意:这类问题的数目往往会很大
//申请二维vector;注意书写形式,以及规模。vector自动初始化为0.
//如果是数组,要手动初始化。
vector<vector<long> > F(n+1,vector<long>(k+1));
//数组第0行:表示用0个骰子,分别凑[0 .. k]的情况
F(0,0)=1; //这里赋值F(0,0);别的都是0不用动。
//数组的第0列,表示用[0..n]个骰子,分别凑0的情况
//这里不用动。
for(int i=1;i<=n;++i){
for(int j=1;j<=k;++j){
//注意下面的写法:没有写h<=6,为什么?
for(int h=1;j-h>=0;++h)
F(i,j)+=F(i-1,j-h);
}
}
}
总共的时间复杂度:O(n*k*6)=O(n*k)
想想有什么优化方法?
- 关于空间:每次只使用当前列和上一列,可以用两个一维数组迭代赋值即可。但会花费数组拷贝的时间。
- 关于时间:其实就是比较F(j)和F(j-1)的关系。每次求和6个数,每次的操作很像队列:进入一个数,弹出一个数。那么使用队列和变量sum,就可以在常数时间内求和了。不过本来就是6个数,就是常数,所以意义不大。
另外:数组迭代时,考虑边界条件,即下标的有效性。
扩展:凑钱问题
凑钱问题是经典的动态规划,这里不多表述。面额: A[1 2 5 10 20],去凑K。那么第i步,就是选取A[0..i]来凑m元。那么F(i)和F(i-1)的唯一区别,就是加入了A[i]面额的钞票。所以:
F(i,m)=F(i-1,m-A[i]*0)+F(i-1,m-A[i]*1)+F(i-1,m-A[i]*2)+...F(i-1,m-A[i]*x)
until: m-A[i]*x<=0
右边的选择分支,表明A[i]面值使用了0次,1次,2次…注意此时选择分支是无固定总数的。这样的时间复杂度通常是O(n*k*x)
,而x=K/A[i],A[i]为常值,故最终为O(n*k^2)
这时候,看看有无优化方法。注意到:
F(i,m-A[i])=F(i-1,m-A[i])+F(i-1,m-A[i]*2)+...
F(i,m)=F(i-1,m-A[i]*0)+F(i,m-A[i])
这样,就可以优化到O(n*k)了。
当然,这样写起来会大大加重代码量。因为上述的所有递归公式,都是在下标有效的基础上设计的。
例如:F(i,m)=F(i-1,m-A[i]*0)+F(i,m-A[i])
前提是m-A[i]>=0,否则只能依次相加。
具体实现暂略,因为没有找到数据验证。
五张点数连续的牌,就是顺子。大小王可以视作任意点数来凑顺子。
输入:五张牌的点数,其中王为0.
这种题目,就属于“思路理清,豁然开朗”的类型。
除了大小王之外的牌依次排序,看看中间有多少空隙,大小王分别可以填一个空隙,两个连在一起可以填两个空隙。所以一开始想,可能要分情况处理。
但是最简单的思路是:
大小王可以做任意牌来填空隙,比较空隙和大小王的数目即可。
关于空隙:A[i]-A[i-1]-1
例如[1 3]的空隙是1,[1 4]的空隙是2
特别注意 :如果两牌相等,则一定不是顺子。
bool IsContinuous( vector<int> numbers ) {
if(numbers.size()!=5)
return false;
sort(numbers.begin(),numbers.end());
int cnt=0;
for(int i=0;iif(numbers[i]>0){
cnt=i;
break;
}
int s=0;
for(int i=cnt;i1;++i){
if(numbers[i+1]==numbers[i])
return false;
s+=numbers[i+1]-numbers[i]-1;
}
if(cnt>=s)
return true;
else
return false;
}
经典的动态规划。重点是在N个人中的幸存者,和N-1个人中的幸存者,是什么关系?
我们看看规模缩减时,情况发生的变化。
N个人的问题:[0 1 2 3 .. N-1]中循环报数,求幸存者。
第k个人死了,游戏继续,此时问题就成了[ k+1 k+2 ..N-1 0 1 2 .. k-1] 这些人继续做游戏。
但是,N-1个人的问题,应该是[0 1 2 3 ..N-2]中循环报数求幸存,所以为了保证子问题的一致性,需要对[ k+1 k+2 ..N-1 0 1 2 .. k-1] ->[0 1 2 3 4 5 6 ..N-2]做映射。不过递归计算是和调用顺序相反的,需要求逆映射。
例如:
N=6
[0 1 2 3 4 5]
k=4
[4 5 0 1 2]->[0 1 2 3 4]
那么[0 1 2 3 4]->[4 5 0 1 2]
实际上是做后者到前者的映射,显然是(x+k)%n
所以说,如果知道F(n-1)中幸存者是x,那么在F(n)中,他的编号应该是(x+k)%n。
int F(int n, int m)
{
if(n==1)
return 0;
return((F(n-1,m)+m)%n);
}
不许用乘除,循环(for,while),条件(if switch ?:)
不许用循环,只能用递归。但递归需要条件判断终止。所以要点是不用条件语句,写出判断来。
关键字:短路求值。
//递归形式1:
//if(A) sum=a;
//else sum=b;
int Sum_Solution1(int n) {
int sum;
if(n==1)
sum=1;
else
sum=n+Sum_Solution1(n-1);
return sum;
}
//递归形式2:
//sum=a;
//if(!A) sum=b;
int Sum_Solution2(int n) {
int sum=1;
if(n!=1)
sum=n+Sum_Solution2(n-1);
return sum;
}
//递归形式3:注意运算优先级
//sum=a;
//A||(sum=b)
int Sum_Solution3(int n) {
int sum=1;
n==1||(sum=n+Sum_Solution3(n-1));
return sum;
}
//递归形式4:注意运算优先级
//sum=a;
//!A&&(sum=b)
int Sum_Solution(int n) {
int sum=1;
n!=1&&(sum=n+Sum_Solution(n-1));
return sum;
}
利用构造函数与静态成员:定义静态成员n和sum,每次构造对象,使得n++;sum+=n;最后使用数组的形式构造多个对象。
class A{
public:
static int n,sum;
A(){
n++;
sum+=n;
}
};
int A::n=0;
int A::sum=0;
int main(){
int t=10;
A* p=new A[t];
delete[] p;
cout<"pause");
}
在这里说一下静态成员:
1. 静态成员可以用逗号,来实现一个语句多个定义。
2. 静态成员本质上是全局变量,全局变量不归属类所有
3. 静态成员需要额外的声明以及初始化。注意,它需要声明,而且是在全局范围内声明。
4. 通常静态成员的声明在类的cpp文件内,它相当于全局范围(因为没有在任何函数内)。
5. 静态成员不归类所有,即使未创建对象,依然存在(从它在全局范围内的声明和初始化开始);即使对象全部析构,它也依然不会消失(完全等效于全局变量)。
利用虚函数求解:实际上用多态来代替条件语句
class A;
A* Array[2];
class A{
public:
virtual int Sum(int n){return 0;}
};
class B:public A{
virtual int Sum(int n){
return Array[!!n]->Sum(n-1)+n;
}
};
int main(){
int n=10;
A a;
B b;
Array[0]=&a;
Array[1]=&b;
return Array[1]->Sum(n);
}
纯C环境下,利用函数指针,和上面一个道理。
typedef int (*fun)(int);
int f(int n){return 0;}
int ff(int n){
static fun f[2]={f,ff};
return n+f[!!n](n-1);
}
还有拿模板类做的,感觉都很花哨。
其实就是拿位运算来模拟加减进位的问题。
比较笨的方法:就像用字符串实现加法一样,从右向左,一位一位,记录相加之后的值以及进位的情况;最后遍历完之后,处理最高的进位。
对于两个位bit1 ,bit2 来说:
相加之后的结果:bit1^bit2 (只有一个是0另一个是1相加才是1)
相加之后的进位:bit1&bit2(只有两个都是1才会进位)
让人惊叹的方法:
比如我们计算十进制加法:
136
298
从右向左,依次是:
6+8结果4进位1
3+9+1结果3进位1
1+2+1结果4进位0
如果我们把结果和进位分开呢?
单单计算结果:324
单单计算进位:011
注意进位是要乘10的,进位表示的真实数值是110
两者再相加:324+110=434
这种做法,是每次两数字相加时,把进位所代表的数,暗暗记在另一笔账上,最后再把账合起来。总之,是拆分后分别相加的做法。
136+298=100+30+6+200+90+8
=(100+200)+(30+90)+(6+8)
=(300)+(20+100)+(4+10)
=300+20+4+100+10
=324+110
用这种做法求出:136+298=324+110的优点在于,第二个数的末端变成了0;继续递归使用它,直至第二个数全0为止。
利用这种思路,不难写出二进制的加法:
int Add(int num1, int num2){
unsigned int res,carry;
while(num2!=0){
res=num1^num2;
carry=num1&num2;
num1=res;
num2=carry<<1;
}
return num1;
}
扩展:不用额外空间交换两个数:
a=a+b;
b=a-b;
a=a-b;
a=a^b;
b=a^b;
a=a^b;
容易想到,定义构造函数为私有,可以防止继承。因为即使是继承,也是站在“第三者”的角度对基类的public方法和成员进行调用。
如果定义构造为私有,只能通过类中其他的public方法,返回new生成的指针:
class A{
public:
static A* make(){return new A();}
static void del(A* p){delete p;}
private:
A(){}
~A(){}
};
注意:因为无法预先创建A的实例,所以当然函数需要是static。
局限性:仅能在堆上创建实例。
新解法:虚继承
虚继承是在多重继承的场合下诞生的,其特点是,派生类的祖先们,对派生类是透明的,可以消除重复继承的问题。
简单理解,虚继承中,派生在创建对象时,会直接调用祖先的构造函数;
一般继承中,派生类调用其父辈,其父辈再分别调用它自己的父辈,来一层层调用来完成。
非虚继承 虚继承
A A A
| | /\
B C B C
\/ \/
D D
template <typename T> class A{
friend T;
private:
A();
~A();
};
class B:virtual public A{
public:
B();
~B();
};
这样B可以自由创建对象,因为它是A的友元,可以调用A的构造;B不可以有派生,因为B的派生也会是虚继承,它会直接调用A的构造,致使失败。