剑指offer 66题 下篇

第四十五题:扑克牌顺子

题目描述
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(n1,m) .
我们最后要求的是f(n,m),表示在n个数字中每次删除第m个数字最后剩下的数字,它相当于删除一个数字k后形成的新序列再继续不断删除第m个数字,最后剩下的数字。
也就是说 f(n,m)=f(n1,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(n1,m)=p[f(n1,m)]
它表示如果映射前的数字是,那么映射后的数字是(x-k-1)%n
代入验证一下可以知道:
映射前数字如果是k-1,映射后的数字是(k-1-k-1)%n相当于n-2。
我们可以求p(x)=(x-k-1)%n的逆映射为: p1(x)=(x+k+1)
其中满足 f(n,m)=f(n1,m)=p1[f(n1,m)]=[f(n1,m)+k+1]%n=[f[n1,m]+(m1)%n+1]%n=[f(n1,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;           
    }         
};

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值