第四十五题:扑克牌顺子
题目描述
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…..LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何。为了方便起见,你可以认为大小王是0。
解题思路
我们将5张牌看成5个数字组成的数组,由于大小王是特殊数字,不妨把它们设置为0,我们可以对数组进行排序,然后用0来补中间的空缺,看能不能都补上。
比如排序之后为0 1 3 4 5 ,刚好有一个0可以用来补上空缺2。
class Solution {
public:
bool IsContinuous( vector<int> numbers ) {
if(numbers.empty())
return false;
sort(numbers.begin(),numbers.end());
int count = (numbers[0]==0)?1:0;
for(int i=1;i<numbers.size();i++){
if(numbers[i]==0)
count++;
if(numbers[i]!=0&&numbers[i]==numbers[i-1]) //如果有两个非0的数相等,不可能是顺子
return false;
}
//跳过前面的0
int i=0;
while(numbers[i]==0) ++i;
int gap =0;
while(i<numbers.size()-1){
gap += numbers[i+1]-numbers[i]-1;
i++;
if(gap>count)
return false;
}
return true;
}
};
第四十六题:孩子们的游戏(圆圈中最后剩下的数)
题目描述
每年六一儿童节,NowCoder都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为NowCoder的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数….这样下去….直到剩下最后一个小朋友,可以不用表演,并且拿到NowCoder名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?
解题思路
先说本题比较容易想到传统的解法,用一个数据结构来模拟这个圆圈,在常用的数据结构中,容易想到环形链表。我们可以创建的环形链表一共有n个结点。每次删除第m个结点。我们可以用std::list来模拟环形结构,由于它本身不是环形结构,所以迭代器扫描到链表末尾时,要把它移动到开头。
class Solution {
public:
int LastRemaining_Solution(unsigned int n, unsigned int m)
{
if(n<1||m<1)
return -1;
list<int> numbers;
for(int i=0;i<n;++i){
numbers.push_back(i);
}
list<int>::iterator cur = numbers.begin();
while(numbers.size()>1){ //当只剩一个小孩时退出循环
for(int i =1;i<m;++i){
cur++;
if(cur==numbers.end())
cur = numbers.begin();
}
list<int>::iterator next = numbers.erase(cur);
if(next == numbers.end())
next = numbers.begin();
cur = next;
}
return (*cur);
}
};
创新的做法,由于是从0开始计数的,第一个被删除的数字是(m-1)%n,记为k,删除k之后剩下的数字是:
0,1,…, k-1, k+1,…,n-1。
下一次删除从数字k+1开始计数,k+1,…,n-1,0,1… k-1
由于这个序列不是从0开始计数的,记为
f′(n−1,m)
.
我们最后要求的是f(n,m),表示在n个数字中每次删除第m个数字最后剩下的数字,它相当于删除一个数字k后形成的新序列再继续不断删除第m个数字,最后剩下的数字。
也就是说
f(n,m)=f′(n−1,m)
这个序列和原来序列之间的映射关系是:
k+1 -> 0
k+2 -> 1
…
n-1 -> n-k-2
0 -> n-k-1
…
k-1 -> n-2
得到映射关系为:p(x)=(x-k-1)%n
f(n−1,m)=p[f′(n−1,m)]
它表示如果映射前的数字是,那么映射后的数字是(x-k-1)%n
代入验证一下可以知道:
映射前数字如果是k-1,映射后的数字是(k-1-k-1)%n相当于n-2。
我们可以求p(x)=(x-k-1)%n的逆映射为:
p−1(x)=(x+k+1)
其中满足
f(n,m)=f′(n−1,m)=p−1[f(n−1,m)]=[f(n−1,m)+k+1]%n=[f[n−1,m]+(m−1)%n+1]%n=[f(n−1,m)+m]%n
我们就得到了一个递归的公式,然而当n=1时,显然序列中只剩下一个开始的数字0,所以我们可以得到:
f(n,m)=0,n=1
int LastRemaining(unsigned int n,unsigned int m){
int(n<1||m<1)
return -1;
int last=0;
for(int i=2;i<=n;i++)//直到模为n
last = (last+m)%i;
return i;
}
第四十七题:求1+2+3+…+n
题目描述
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
解题思路
如果不用循环而考虑用递归的时候,由于要考虑终止条件,所以不适用。
思路一: 我们可以考虑用构造函数求解,先定义一个类型,然后创建n个该类型的实例,所以这个构造函数将被调用n次。
class Solution {
public:
Solution(){
++N;
sum+=N;
}
int Sum_Solution(int n) {
Solution::reset();
Solution *p = new Solution[n];
delete []p;
p = NULL;
return Solution::getResult();
}
static void reset(){ sum =0; N=0;};
static int getResult(){return sum;};
private:
static int sum;//求和
static int N;//当前要加的数i
};
int Solution::sum =0;
int Solution::N =0;
思路二:利用虚函数求解
我们可以用一个函数充当递归函数的角色,另一个函数处理终止递归的情况,我们要从两个函数里二选一,自然而然会想到用布尔变量,值为true时调用第一个,false时调用第二个函数。如果把数值转换为布尔,可以通过对n连续做两次求反运算!!n。非零的n转化为true,0转换为false。
思路三:利用函数指针求解
用函数指针来模拟:
typedef unsigned int (*fun) (unsigned int);
unsigned int Solution_Terminator(unsigned int n){
return 0;
}
unsigned int Sum_solution(unsigned int n){
static fun f[2]={Solution_Terminator,Sum_solution};
return n+f[!!n](n-1);
}
第四十八题:不用加减乘除做加法
题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
解题思路
第一步,只做相加,不进位。
0和1得到1, 1和0得到1 ,0和0得到0,1和1得到0。这可以通过异或运算来实现。
第二步,做进位。
可以想象两个数先做位与运算,然后再向左移动一个位,只有两个数都是1时,位与运算得到结果为1,其余都是0。
第三步,把前面两个结果加起来。
重复前面两个运算,直到不出现进位为止。
class Solution {
public:
int Add(int num1, int num2)
{
int sum, carry=1;
while(carry){
sum = num1^num2;
carry = (num1& num2)<<1;
num1 = sum;
num2 = carry;
}
return num1;
}
};
第四十九题:把字符串转换成整数
题目描述
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。
解题思路
要考虑正负数,以及是否有非法输入,对于不正确的值应该用一个值表示
class Solution {
public:
int StrToInt(string str) {
if(str.empty())
return 0;
bool minus=false;
long long num=0;
int i=0;
if(str[i]=='+'){
i++;
}
if(str[i]=='-')
{
i++;
minus = true;
}
for(;i<str.length();++i){
if(str[i]>='0'&&str[i]<='9')
{
num = num*10+str[i]-'0';
if((!minus&&num>0x7FFFFFFF)||
(minus&& (-1*num)<(signed int)0x80000000)){//用 long long 来存储,判断超过int的范围
num = 0;
break;
}
}
else{
num = 0;
break;
}
}
try{ //考虑非法的输入抛出异常的情况,注释掉这部分可以通过牛客的oj
if(i!=str.length()){
throw "invalid input";
}
}
catch(char* s){
cerr<<s<<endl;
exit(1);
}
num = (int)(minus?num*-1:num);
return num;
}
};
第五十题:数组中重复的数字
题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3。
解题思路
解法一:传统的做法,使用哈希表:时间复杂度和空间复杂度都是
O(n)
。
class Solution {
public:
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
bool duplicate(int numbers[], int length, int* duplication) {
if(numbers==NULL||length<=1)
return false;
int * save = new int[length]();
for(int i=0;i<length;++i){
save[numbers[i]]++;
if(save[numbers[i]]>1){
*duplication=numbers[i];
return true;
}
}
delete []save;
return false;
}
};
解法二:交换位置
参考:
http://www.cnblogs.com/AndyJee/p/4693099.html
0~n-1正常的排序应该是A[i]=i;因此可以通过交换的方式,将它们都各自放回属于自己的位置;
从头到尾扫描数组A,当扫描到下标为i的数字m时,首先比较这个数字m是不是等于i,
如果是,则继续扫描下一个数字;
如果不是,则判断它和A[m]是否相等,如果是,则找到了第一个重复的数字(在下标为i和m的位置都出现了m);如果不是,则把A[i]和A[m]交换,即把m放回属于它的位置;
重复上述过程,直至找到一个重复的数字;
时间复杂度:O(n),空间复杂度:O(1)
(将每个数字放到属于自己的位置最多交换两次,一次是主动,一次是被动)
class Solution {
public:
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
bool duplicate(int numbers[], int length, int* duplication) {
if(numbers==NULL||length<=1)
return false;
int i=0;
while(i<length){
if(numbers[i]==i){
i++;
}else if(numbers[numbers[i]]!=numbers[i]){
swap(numbers[i],numbers[numbers[i]]);
}else{
*duplication = numbers[i];
return true;
}
}
return false;
}
};
第五十一题:构建乘积数组
题目描述
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…A[i-1]*A[i+1]…*A[n-1]。不能使用除法。
解题思路
来自网友张:
对于第一个for循环
第一步:b[0] = 1;
第二步:b[1] = b[0] * a[0] = a[0]
第三步:b[2] = b[1] * a[1] = a[0] * a[1];
第四步:b[3] = b[2] * a[2] = a[0] * a[1] * a[2];
第五步:b[4] = b[3] * a[3] = a[0] * a[1] * a[2] * a[3];
然后对于第二个for循环
第一步
temp *= a[4] = a[4];
b[3] = b[3] * temp = a[0] * a[1] * a[2] * a[4];
第二步
temp = a[3] = a[4] a[3];
b[2] = b[2] * temp = a[0] * a[1] * a[4] * a[3];
第三步
temp = a[2] = a[4] a[3] * a[2];
b[1] = b[1] * temp = a[0] * a[4] * a[3] * a[2];
第四步
temp = a[1] = a[4] a[3] * a[2] * a[1];
b[0] = b[0] * temp = a[4] * a[3] * a[2] * a[1];
由此可以看出从b[4]到b[0]均已经得到正确计算。
class Solution {
public:
vector<int> multiply(const vector<int>& A) {
vector<int> result(A.size());
if(A.empty())
return result;
int b = 1;
//result[i]=data[0]*data[1]...data[i-1]
for(int i=0;i<A.size();++i){
result[i] = b;
b*=A[i];
}
b = 1;
//data[n-1]*data[n-2]...data[i+1]
for(int i = A.size()-1;i>=0;--i){
result[i] = b*result[i];
b*=A[i];
}
return result;
}
};
第五十二题:正则表达式匹配
题目描述
请实现一个函数用来匹配包括'.'
和'*'
的正则表达式。模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串”aaa”与模式”a.a”和”ab*ac*a”匹配,但是与”aa.a”和”ab*a”均不匹配
解题思路
如果模式串此时是’.’,那么只需要模式串与匹配串都往后移动一个位置即可
如果模式下一位是*
如果现在这位的字符能匹配,我们则需要分情况讨论
1.匹配串往后移动1位,模式串跳过’*’ ,模式串加2-> 匹配1个字符,比如匹配串是abc,模式串是ab*c(b出现了1次)
2.匹配串往后移动1位,模式串不动,->匹配多个字符,匹配串是abbc,模式串是ab*c
3.匹配串不动,模式串跳过’*’ 匹配0个字符,什么意思呢,注意读题,比如说匹配串是ac,模式串是ac*c
是可以的,因为c可以出现0次。这样一来还是匹配。
如果pttern当前字符和和str的当前字符不匹配,即pttern当前字符能匹配 str 中的 0 个字符:(str, pattern+2),
比如匹配串abc,模式串ac*bc
class Solution {
public:
bool match(char* str, char* pattern)
{
if(NULL==str&&NULL==pattern)//都是空字符串
return true;
if(NULL==str||NULL==pattern)//其中一个为空,则不匹配
return false;
return matchS(str,pattern);
}
bool matchS(char* str,char*pattern){
if(*str=='\0'&& *pattern=='\0')//同时到字符串末尾,表示匹配
return true;
if(*str!='\0'&& *pattern=='\0')//原串没到末尾,模式串到末尾,不匹配
return false;
if(*(pattern+1)=='*')
{
if(*pattern == *str || (*pattern=='.' && *str != '\0'))
return matchS(str+1,pattern+2)||matchS(str+1,pattern)||matchS(str,pattern+2);
else
return matchS(str,pattern+2);
}
if(*str==*pattern || *str!='\0'&& *pattern=='.'){
return matchS(str+1,pattern+1);
}
return false;
}
};
第五十三题:表示数值的字符串
题目描述
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串”+100”,”5e2”,”-123”,”3.1416”和”-1E-16”都表示数值。 但是”12e”,”1a3.14”,”1.2.3”,”+-5”和”12e+4.3”都不是。
解题思路
首先是正负号,接下来应该是一串数字,也可以没有数字后面直接跟小数点,.12也是合法的。
如果有小数点,那么小数后面同样应为数字,
如果有e,后面首先检查有没有符号位,然后扫描数字。
class Solution {
public:
bool isNumeric(char* string)
{
if(!string)
return false;
char* p = string;
if(*p=='+'||*p=='-') //有符号跳过
++p;
if(!scanDigit(&p))
return false;
if(*p!='\0'){
//if((*p!='.')&&(*p!='e'&&*p!='E'))
// return false;
if(*p =='.'){ //小数点
++p;
scanDigit(&p); //比如-.这种情况,这里默认是true
if(*p=='\0')
return true;
}
if(*p == 'e'||*p=='E'){
++p;
if(*p=='\0')
return false;
if(*p=='+'||*p=='-')
++p;
if(!scanDigit(&p))
return false;
return *p=='\0'; //指数部分除去符号必须遍历完
}
return false;//如果有别的可能,比如1.2.3
}else //整数遍历完
return true;
}
bool scanDigit(char** p ){
if(**p=='\0') //应该有数字的部分没有数字则返回false
return false;
//if(!(**p<='9'&&**p>='0'))//首位必须为数字
// return false;
while(**p!='\0'&&(**p<='9')&&(**p>='0'))
++(*p);
return true;
}
};
第五十四题:字符流中第一个不重复的字符
题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符”go”时,第一个只出现一次的字符是”g”。当从该字符流中读出前六个字符“google”时,第一个只出现一次的字符是”l”。
解题思路
可以定义一个数据容器来保存字符在字符流中的位置。当一个字符第一次从字符流中读出来时,也就是默认初始值-1时,把它在字符流中的位置保存到数据容器里(所以需要一个记录位置的变量,如下文中的index)。当这个字符再次从字符流中被读出来时如果发现标记不为-1,那么它就不是只出现一次的字符,也就可以被忽略了,这时把它在数据容器里保存的值更新成一个特殊的值(比如-2)。最后在数组中找大于0的最小位置值。
class Solution
{
public:
Solution(){
arr = new int[128];
for(int i=0;i<128;++i)
arr[i]=-1;
index = 0;
}
~Solution(){
delete []arr;
}
//Insert one char from stringstream
void Insert(char ch)
{
if(arr[ch]==-1)
arr[ch]=index++;//注意这里保存的是位置
else
arr[ch]=-2;
str+=ch;
}
//return the first appearence once char in current stringstream
char FirstAppearingOnce()
{
if(str.empty())
return '#';
int minIndex=~(1<<31);//整数最大值
char ch='#';
for(int i=0;i<128;++i){
if(arr[i]>=0 && arr[i]<minIndex){
ch = (char) i;// 把下标int转化为字符型
minIndex = arr[i]; //出现的时间最小
}
}
return ch;
}
private:
string str;
int index;
int* arr;
};
第五十五题:链表中环的入口结点
题目描述
一个链表中包含环,请找出该链表的环的入口结点。
解题思路
很容易想到用快慢指针的思路,如果我们预先知道环有n个结点,让快指针先走n步,那么当慢指针到达环的入口时,此时快指针比它领先n步,刚好也在环的入口结点。(注意此时它们的运动速度相同,但是单位长度)
至于如果计算环中结点的数目,同样可以利用快慢指针,如果链表中存在环,那么它们相遇的位置一定在环中。可以从这个相遇结点出发,一边计数一边向前移动,直到再次回到该结点。
另一种思路,在相遇的时候,快指针其实比慢指针正好多走了一圈,而它的运动速度是慢指针的两倍,所以慢指针这时实际走的步数就等于环中结点的数目。我们把快指针指向链表开头,这时慢指针比它多走了n步,它们再次相遇时必然在环的入口位置。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if(!pHead)
return NULL;
ListNode * p1 = pHead;
ListNode * p2 = pHead;
while(p2 && p2->next){
p1 = p1->next;
p2 = p2->next->next;
if(p1 == p2)
break;
}
if(!p2||!p2->next)
return NULL;
/* 从相遇结点开始遍历一圈,找出圆圈中的数目
ListNode * p = p2;
int c=0;
while(p!=p2){
++p; ++c;
}
p1=p2=pHead;
for(int i=0;i<c;++i){
p2 = p2->next;
}*/
//相遇时,p2正好比p1多走了一圈
p2 = pHead;
while(p1!=p2){
p1 = p1->next; p2 = p2->next;
}
return p2;
}
};
第五十六题:删除链表中重复的结点
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
解题思路
解决这个问题的第一步是确定删除的参数。当然这个函数需要输入待删除链表的头结点。头结点可能与后面的结点重复,也就是说头结点也可能被删除,所以在链表头添加一个结点。
如果当前结点和下一个结点相同,我们要删除重复结点时,首先找到全部的重复结点,但是在删除时,我们还需当前结点的前驱结点,所以需要我们还需要一个prev指针。
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if(!pHead)
return pHead;
int val = 0;
ListNode * pFirst = new ListNode(val);
pFirst->next = pHead;
ListNode * pre = pFirst;
pre->next = pHead;
ListNode * p = pHead;
while(p && p->next){
if(p->val == p->next->val){
int val = p->val;
while(p && (p->val == val)) //当p值发生变化时退出循环
{
ListNode* tmp = p;
p = p->next;
delete tmp;
}
pre->next = p;
}
else{ //相邻两个节点不相同
//pre->next = p;
pre = pre->next;
p = p->next;
}
}
return pFirst->next; //跳过头结点
}
};
第五十七题:二叉树的下一个结点
题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
解题思路
(1)如果一个结点有右子树,那么它的下一个结点就是它的右子树中的左子结点。也就是说右子结点出发一直沿着指向左子结点的指针,我们就能找到它的下一个结点。
(2)接着我们分析一个结点没有右子树的情形。如果结点是它父节点的左子结点,那么它的下一个结点就是它的父结点。
(3)如果一个结点既没有右子树,并且它还是它父结点的右子结点,这种情形就比较复杂。我们可以沿着指向父节点的指针一直向上遍历,直到找到一个是它父结点的左子结点的结点。如果这样的结点存在,那么这个结点的父结点就是我们要找的下一个结点
struct TreeLinkNode {
int val;
struct TreeLinkNode *left;
struct TreeLinkNode *right;
struct TreeLinkNode *next;
TreeLinkNode(int x) :val(x), left(NULL), right(NULL), next(NULL) {
}
};
class Solution {
public:
TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
if(!pNode)
return pNode;
if(pNode->right!=NULL){// 如果有右子树就是右子树的左子结点
TreeLinkNode * p = pNode->right;
while(p->left){
p = p->left;
}
return p;
}
else if(pNode->next){
TreeLinkNode * par = pNode->next;//父节点
TreeLinkNode * p = pNode;
while(par && p!=par->left){//如果它不是父节点的左结点,向上遍历直到它是父节点的左子结点
p = p->next;
par = par->next;
}
return par; ///可能也会return NULL
}
return NULL;
}
};
第五十八题:对称的二叉树
题目描述
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
解题思路
可以采用递归来实现,先看当前结点的左子树和右子树是否都存在,如果都存在比较它们的值是否相同,然后递归的同时比较左子树的左子树和右子树的右子树 与 左子树的右子树和 右子树的左子树是否都相同。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot)
{
return isSysm(pRoot,pRoot);
}
//以p1 p2为根节点的两棵树是否镜像对称
bool isSysm(TreeNode* p1, TreeNode* p2){
if(!p1 && !p2)
return true;
if( (!p1)||(!p2))
return false;
if(p1->val != p2->val)
return false;
return isSysm(p1->left,p2->right)&&isSysm(p1->right,p2->left);
}
};
第五十九题:按之字形顺序打印二叉树
题目描述
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
解题思路
按之字形顺序打印二叉树需要两个栈。我们在打印某一行结点时,把下一层的子结点保存到相应的栈里。如果当前打印的是奇数层,则先保存左子结点再保存右子结点到一个栈里;如果当前打印的是偶数层,则先保存右子结点再保存左子结点到第二个栈里。
下面这种写法来自牛客网友,用两个栈来回倒。实现起来也很直观。
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int> > result;
stack<TreeNode *> stack1,stack2;
bool direction = true;//向右打印为true,向左打印为false
if(pRoot!=NULL)
stack1.push(pRoot);
struct TreeNode *node;
while(!stack1.empty() || !stack2.empty()){
vector<int> data;
if(!stack1.empty()){
while(!stack1.empty()){
node = stack1.top();
stack1.pop();
data.push_back(node->val);
if(node->left!=NULL)
stack2.push(node->left);
if(node->right!=NULL)
stack2.push(node->right);
}
result.push_back(data);
}
else if(!stack2.empty()){
while(!stack2.empty()){
node = stack2.top();
stack2.pop();
data.push_back(node->val);
if(node->right!=NULL)
stack1.push(node->right);
if(node->left!=NULL)
stack1.push(node->left);
}
result.push_back(data);
}
}
return result;
}
下面这种做法也可以,用层序遍历的方法,分层打印,第二层需要从右往左的输出,我们可以选择从左往右的压入。
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int> > result;
if(!pRoot)
return result;
stack<TreeNode*> lastStack;//上一层的元素用栈表示
lastStack.push(pRoot);
int current = 1 ;//当前层的个数
bool isOddLayer = true;//如果是奇数层
while(!lastStack.empty()){
vector<int> t;
stack<TreeNode*> layerStack;//存放一层元素的临时变量
int nextCount=0;//下一层的个数
for(int i=0;i<current;++i){
TreeNode* e = lastStack.top();
lastStack.pop();
t.push_back(e->val);
if(isOddLayer){
if(e->left){
layerStack.push(e->left); //因为是栈的结构所以要临时存储
++nextCount;
}
if(e->right){
layerStack.push(e->right);
++nextCount;
}
}else{
if(e->right){
layerStack.push(e->right);
++nextCount;
}
if(e->left){
layerStack.push(e->left); //因为是栈的结构所以要临时存储
++nextCount;
}
}
}
isOddLayer = !isOddLayer;
lastStack = layerStack;
current = nextCount;
result.push_back(t);
}
return result;
}
};
第六十题:把二叉树打印成多行
题目描述
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
解题思路
用一个队列来保存将要打印的结点。为了把二叉树的每一行单独打印到一行里,我们需要两个变量:一个变量表示在当前的层中还没有打印的结点数,另一个变量表示下一次结点的数目。
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int> > result;
if(!pRoot)
return result;
queue<TreeNode*> lis;
lis.push(pRoot);
int current = 1 ;//当前层的个数
while(!lis.empty()){
vector<int> t;
int nextCount=0;//下一层的个数
for(int i=0;i<current;++i){
TreeNode* e = lis.front();
lis.pop();
t.push_back(e->val);
if(e->left){
lis.push(e->left);
++nextCount;
}
if(e->right){
lis.push(e->right);
++nextCount;
}
}
current = nextCount;
result.push_back(t);
}
return result;
}
};
第六十一题:序列化二叉树
题目描述
请实现两个函数,分别用来序列化和反序列化二叉树
解题思路
我们可以根据前序遍历的顺序来序列化二叉树,因为前序遍历是从根结点开始的。当在遍历二叉树碰到NULL指针时,这些NULL指针序列化成一个特殊的字符,下文使用的是一个标记位0xFFFF。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
char* Serialize(TreeNode *root) {
if(!root)
return NULL;
vector<int> tmp;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
TreeNode* e = q.front();
q.pop();
if(!e) //如果为空指针
tmp.push_back(0xFFFF); //本来想存成char,后来发现char能包含的范围太小
else{
tmp.push_back(e->val);
q.push(e->left);
q.push(e->right);
}
}
int *result = new int[tmp.size()];
memcpy(result, &tmp[0], tmp.size()*sizeof(int));
return (char*)result;//转为字符指针
}
TreeNode* Deserialize(char *str) {
if(!str)
return NULL;
int * p = (int*)str; //在使用的时候再转换为int指针
if(*p==0xFFFF)
return NULL;
queue<TreeNode*> e;
TreeNode* root = new TreeNode(*p);//树的根节点
e.push(root);
//用非递归的方式完成
while(!e.empty()){
TreeNode* tmp = e.front();
e.pop();//弹出头节点
if(*(++p)!=0xFFFF){ //不为空节点
tmp->left = new TreeNode(*p);
e.push(tmp->left);
}
else{
tmp->left = NULL;
}
if(*(++p)!=0xFFFF){//不为空节点
tmp->right = new TreeNode(*p);
e.push(tmp->right);
}
else
tmp->right = NULL;
}
return root;
}
};
第六十二题:二叉搜索树的第k个结点
题目描述
给定一颗二叉搜索树,请找出其中的第k大的结点。例如, 5 / \ 3 7 /\ /\ 2 4 6 8 中,按结点数值大小顺序第三个结点的值为4。
解题思路
如果按照中序遍历的顺序遍历一棵二叉搜索树,遍历序列的数值是递增排序的。只需要用中序遍历算法遍历一棵二叉搜索树,就很容易找出它的第k大结点。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
TreeNode* KthNode(TreeNode* pRoot, unsigned int k)
{
if(!pRoot || k<1)
return NULL;
return findNode(pRoot,k);
}
TreeNode * findNode(TreeNode* pRoot,unsigned int& k){
TreeNode* result = NULL;
if(pRoot->left)
result = findNode(pRoot->left,k);
if(!result){//左子树没有找到
if(k==1)
return pRoot;//当前是第k个数故返回
else
k--;
}
if(!result&& pRoot->right){
result = findNode(pRoot->right,k);
}
return result;
}
};
第六十三题:数据流的中位数
题目描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
解题思路
方案一
为了避免二叉搜索树的最差情况,还可以利用平衡的二叉搜索树,即 AVL 树。通常 AVL 树的平衡因子是左右子树的高度差。可以稍作修改,把 AVL 的平衡因子改为左右子树结点数目之差。有了这个改动,可以用 O(logn)时间往 AVL 树中添加一个新结点,同时用 O(1)时间得到所有结点的中位数。
方案二
如果能够保证数据容器左边的数据都小于右边的数据,这样即使左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。如何快速从一个容器中找出最大数?用最大堆实现这个数据容器,因为位于堆顶的就是最大的数据。同样,也可以快速从最小堆中找出最小数。 因此可以用如下思路来解决这个问题:用一个最大堆实现左边的数据容器,用最小堆实现右边的数据容器。往堆中插入一个数据的时间效率是 O(logn)。由于只需 O(1)时间就可以得到位于堆顶的数据,因此得到中位数的时间效率是 O(1)。
class Solution {
private:
vector<int> left;// max heap
vector<int> right;//min heap
public:
void Insert(int num)
{
if(left.size()==right.size()){ //如果为奇数在左边多放一个
if(left.empty()){
left.push_back(num);return;
}
if(num>right.front()){//比最小堆的最小值大,应该放在右边,此时把原最小值放到左边,这样右边的数目不变,左边增加1
//右边的最小值放到左边
left.push_back(right.front());
push_heap(left.begin(),left.end());
//num放在右边,替换右边的最小值
pop_heap(right.begin(),right.end(),greater<int>());//默认为max heap,这里使用极小堆所以用到了二元谓词
right.back()=num;
push_heap(right.begin(),right.end(),greater<int>());
}
else{ //比最小堆最小值还小,所以应该放在左边
left.push_back(num);
make_heap(left.begin(),left.end());
}
}
else{ //左边的数目只可能大于或等于右边的数目,这里左边的数目比右边的数目多
if(num>=left.front()){//比最大堆的最大值大,应该放在右边
right.push_back(num);
push_heap(right.begin(),right.end(),greater<int>());
}
else{
//先从左边取出最大值放到右边,然后再将num放到左边去
//这样左边的数目不变
right.push_back(left.front());
push_heap(right.begin(),right.end(),greater<int>());
pop_heap(left.begin(),left.end());
left.back()=num;
push_heap(left.begin(),left.end());
}
}
}
double GetMedian()
{
if(left.size()==right.size()){
return (left.front()+right.front())/2.0;
}
return left.front();
}
};
第六十四题:滑动窗口的最大值
题目描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
解题思路
一个滑动窗口可以看成是一个队列。这个问题就转化成如何从队列中找出它的最大值的问题。并且,我们需要的是一个双端开口的队列。
假设为 2 3 4 2 6 2 5 1
假设这个过程是从到右的过程。数组的第一个数字是 2,把它存入队列中。第二个数字是3.由于它比前一个数字 2 大,因此 2不可能成为滑动窗口中的最大值。2 先从队列里删除,再把3存入到队列中。此时队列中只有一个数字 3。针对第三个数字 4 的步骤类似,最终在队列中只剩下一个数字 4。此时滑动窗口中已经有 3 个数字,而它的最大值 4 位于队列的右端,也就是尾端。
接下来处理第四个数字 2。2 比队列中的数字 4 小。当 4 滑出窗口之后 2 还是有可能成为滑动窗口的最大值,因此把 2 存入队列的尾部。现在队列中有两个数字 4 和 2,其中最大值 4 仍然位于队列的左端。
依次类推,只是我们要注意滑动窗口的长度影响着队列中左端值什么时候过期,过期了就把它弹出队列。
所以我们可以在这个双端队列中存入数组下标值,而不是元素值。
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
if(num.empty()||size==0)
return vector<int>();
vector<int> result(num.size()-size+1,0);
int current=0;
deque<int> tmp;
for(int i=0;i<num.size();++i){
while(!tmp.empty() && num[tmp.back()]<= num[i]){
tmp.pop_back();
}
tmp.push_back(i);
if(tmp.front()==i-size)
tmp.pop_front();
if(i>=size-1){//比如0 1 2 3 4 5,窗口长度为3,从下标2开始就可以记录结果了
result[current++] = num[tmp.front()];
}
}
return result;
}
};
第六十五题:矩阵中的路径
题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串”bcced”的路径,但是矩阵中不包含”abcb”路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
解题思路
回溯法的经典问题,由于其递归特性,路径可以被开成一个栈。当在矩阵中定位了路径中前 n 个字符的位置之后,在与第 n 个字符对应的格子的周围都没有找到第 n+1 个字符,这个时候只要在路径上回到第 n-1 个字符,重新定位第 n 个字符。
class Solution {
public:
bool hasPath(char* matrix, int rows, int cols, char* str)
{
if(!matrix||!str)
return false;
if(rows==0||cols==0)
return false;
checked.resize(rows*cols);
for(int i=0;i<checked.size();++i)
checked[i] = false;
int arrLength =0;
for(int i=0;i<rows;++i){
for(int j=0;j<cols;++j){
if(findCore(matrix,rows,cols,arrLength,str,i,j))
return true;
}
}
return false;
}
bool findCore(char* matrix,int rows,int cols,
int& arrLength,char* str,int i,int j){
if(arrLength == strlen(str))
return true;
if(i<0||i>=rows ||j<0||j>=cols)
return false;
if(checked[i*cols+j])
return false;
if(matrix[i*cols+j]==str[arrLength]){
arrLength++;
checked[i*cols+j]=true;
if( findCore(matrix,rows,cols,arrLength,str,i,j-1)||
findCore(matrix,rows,cols,arrLength,str,i-1,j)||
findCore(matrix,rows,cols,arrLength,str,i,j+1)||
findCore(matrix,rows,cols,arrLength,str,i+1,j))
return true;
// else trace back
checked[i*cols+j]=false;
arrLength--;
}
return false;
}
private:
vector<bool> checked;
};
第六十六题:机器人的运动范围
题目描述
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解题思路
和前面的矩阵中的路径类似,这个方格也可以看出一个 m*n 的矩阵。同样在这个矩阵中,除边界上的格子之外其他格子都有四个相邻的格子。
机器人从坐标(0,0)开始移动。当它准备进入坐标为(i,j)的格子时,通过检查坐标的数位和来判断机器人是否能够进入。如果机器人能够进入坐标为(i,j)的格子,我们接着再判断它能否进入四个相邻的格子(i,j-1)、(i-1,j),(i,j+1) 和 (i+1,j)。
class Solution {
private:
vector<bool>visited;
public:
int movingCount(int threshold, int rows, int cols)
{
if(threshold <0 || rows<1 ||cols<1){
return 0;
}
visited.resize(rows*cols);
visited={false};
return movingCountCore(threshold,rows,cols,0,0);
}
int movingCountCore(int threshold,int rows,int cols,
int i,int j){
if(i<0||j<0||i>=rows||j>=cols)
return 0;
if(visited[i*cols+j])
return 0;
if(getDigitSum(i)+getDigitSum(j)>threshold)
return 0;
visited[i*cols+j] = true;
return 1 + movingCountCore(threshold,rows,cols,i,j-1)+
movingCountCore(threshold,rows,cols,i-1,j)+
movingCountCore(threshold,rows,cols,i,j+1)+
movingCountCore(threshold,rows,cols,i+1,j);
}
int getDigitSum(int num){
int result = 0;
while(num){
result+=(num % 10);
num /= 10 ;
}
return result;
}
};