剑指offer66题 中篇

万钟则不辩礼义而受之,万钟于我何加焉。—-孟子告子上

第二十三题:二叉搜索树的后序遍历序列

题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
解题思路
二叉搜索树的特点,左子树比根结点小,右子树都比根结点大。后序遍历的特点,最后一个数字是根结点。所以我们找到比根结点小的部分,其最后一个数字,应该是左子树的根结点,对于右子树亦然,应该都是比根结点大。但是如果我们发现右子树中存在一个比根结点小的值,就不可能是某二叉搜索树的后序遍历的结果。

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        if(sequence.empty())
           return false;
        return verify(sequence,0,sequence.size()-1);

    }

    bool verify(vector<int>& sequence,int begin,int end){
        if(begin>end)
            return false;      
        int root = sequence[end];// 最后一个结点
        int i;
        //遍历直到发现比根结点大的结点
        for( i =begin ;i<end;i++){
            if(sequence[i]>root)
                break;           
        }
        //检查这个结点右边的部分,如果有的话,是否都比根结点大。
        for(int j=i+1;j<end;j++){
            if(sequence[j]<root)
                return false;
        }
        //left if not empty
        bool leftCheck =true;bool rightCheck = true;
        //递归调用,检查左子树是不是搜索树
        if(i>begin){         
           leftCheck   =  verify(sequence,begin,i-1);           
        }          
        //检查右子树是不是搜索树
        if(i<end)
            rightCheck =  verify(sequence,i,end-1);
        return leftCheck && rightCheck;          
    };
};

第二十四题:二叉树中和为某一值的路径

题目描述
输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
解题思路
当用前序遍历的方式访问都某一结点时,我们把该结点添加到路径上,并累加该结点的值。如果该结点为叶结点并且路径中结点值的和刚好等于输入的整数,则当前的路径符合要求,我们把它打印出来。
如果当前结点不是叶结点,则继续访问它的子结点,当前结点访问结束后,递归函数将自动回到它父结点的,在退出之前要在路径上删除当前结点,并在累加值中减去它的值。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};*/
class Solution {
public:
    vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
        vector<vector<int> > result;       
        vector<int> tmp;
        findPath(root,tmp,expectNumber,0,result);
        return result;       
    }
    void findPath(TreeNode* p,vector<int>& path,int sum,int cur,vector<vector<int> >& result){

        if(!p)
            return;
        cur += p->val;
        path.push_back(p->val);
        if(sum == cur && !(p->left) && !(p->right)){
            result.push_back(path);           
        }
        if(p->left)
            findPath(p->left,path,sum,cur,result);
        if(p->right)
            findPath(p->right,path,sum,cur,result);
        path.pop_back();         
    }      
};

第二十五题:复杂链表的复制

题目描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点)。
解题思路
常规的思路是先复制每一个结点,用next指针连接起来,随后对于原来每个sibiling指针,通过计算它离头结点的步数,指向新的链表中的结点。这样做的时间复杂度是 O(n2)

第二种思路相当于是空间换时间,用O(n)的哈希表来实现。
第三种方法,在不用辅助空间的情况下实现O(n)的效率,就是仍然根据原来链表中每个结点 N 创建对应的N,但是把 N 放在N的后面。
这样我们得到a ->a’-> b-> b’-> c-> c’这样形式的链表 ,当原始链表上的a的sibling指向结点c时,其对应复制出来的a’是也应该指向c’,而c’就是c的next。不需要从头遍历链表就能定位。
接下来要做的就是拆分这两个链表。把奇数点的位置用next连接起来就是原始链表,把偶数点的位置用next连接起来就是新的链表。

/*
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)
            return NULL;
        RandomListNode * p1 = pHead;
        // 原来每个结点后面复制插入新的结点
        while(p1){
            RandomListNode * p2  = new RandomListNode(p1->label);
            p2->next = p1->next;
            p1->next = p2;
            p1 = p2->next;             
        }
        //复制随机的结点
        // random
        p1 = pHead;     
        while(p1){
            RandomListNode * p2=p1->next;
            if(p1->random)
                p2->random = p1->random->next;
            p1 = p2->next ;//jump over cloned one          
        }
        //separate 拆分,下面这种做法就是简单的遍历,一次把一个结点指向它后面的后面
        //在指向之前,先保留它原来的后继结点         
        p1 = pHead;
        RandomListNode * head2 = p1->next;
        RandomListNode * p2;
        while(p1->next){
            p2 = p1->next;//2 3 4
            p1->next = p2->next ;//1->3 2->4 3->5...
            p1  = p2;//2 3         
        }
        return head2;               
    }
};

第二十六题:二叉搜索树与双向链表

题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。要求转换后的双向连表示排号序的。
解题思路
在二叉树中,每个结点都有两个指向子结点的指针,左右子树,而双向链表中,分别是前驱和后继。
我们调整的时候,原先指向左子树的指针调整成前驱指针,指向右子树的指针调整为后继指针。
由于要求最后的结果是排好序的,我们先中序遍历树中的每一个结点,当遍历到根结点时,把树看成三个部分,根结点,左子树和右子树,我们把根结点和左子树的最大的结点连接起来,同时和右子树的最小的结点连接起来。
按照中序遍历的顺序 左 中 右
当我们遍历到根结点时,它的左子树已经转换成排好序的链表了,接着我们遍历右子树,把根结点和右子树中最小的结点连接起来。随后对它的右子树递归,这个递归和转换的过程是同步的。
中序遍历过程中要记录上一个遍历到的结点,可以用一个指向指针的指针来实现参数的传递。

/*
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 NULL;
        TreeNode* result = NULL;
        convertTree(pRootOfTree,&result);
        //移动到最左结点
        TreeNode* firstNode = result;
        while(firstNode != NULL && firstNode->left!=NULL)
            firstNode = firstNode->left;
        return firstNode;        
    }

    void convertTree(TreeNode* current,TreeNode** lastLeftList){

        if(!current)
            return ;       
        if(current->left)//如果存在左子树,那么递归在叶子层返回,对应于中序遍历左子树的递归
            convertTree(current->left,lastLeftList);
        current->left = *lastLeftList;// 当前结点前驱结点指向双向链表的最右端
        if(*lastLeftList)
            (*lastLeftList)->right = current;// 如果非空,左侧双向链表最右端指向当前根结点
        *lastLeftList = current;//双向链表的最右侧的指针右移
        if(current->right) //遍历和转换右子树
            convertTree(current->right,lastLeftList);               

    } 

};

第二十七题:字符串的排列

题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 结果请按字母顺序输出。
解题思路
我们把一个字符串看成两个部分,一部分为第一个位置的字符,第二部分为它后面所有的字符。首先求所有可能出现在第一个位置的字符,这个通过把这个字符后面每一个字符交换,第二步,就是固定第一个字符,求后面所有字符的排列,这个时候就可以递归的求解。
递归退出的条件,就是当这个序列长度为1时,这时就得到了一个排列的结果。

class Solution {
public:
    vector<string> Permutation(string str) {
        vector<string> result;
        if(str.empty()){
            return result;            
        }

        permute(str,0,result);
        sort(result.begin(),result.end());
        return result;

    }

    void permute(string str, int start,vector<string>& result){
        if(start==str.length()-1){//当起点位置为最后一位时,代表获得了一个排列
            result.push_back(str);
            return;
        }

        for(int j = start;j < str.length();j++){
            if(j!=start && str[start]==str[j])//重复字符,那么不要交换
                continue;
            swap(str[start],str[j]);        //start==j和本身交换的情况,也就是不交换   
            permute(str,start+1,result);
            swap(str[start],str[j]);   //随后要交换回来,abc变成bac后再变回,随后abc变为cba.保证是第一位和后面的若干位依次交换            
        }           

    }
};

第二十八题:数组中出现次数超过一半的数字

题目描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解题思路
解法一
转换一下对题目的解读,这个数字如果长度超过了数组长度的一半,那么数组的中位数必然是它,也就是说如果数组有n个数,那么就有n/2个数比它小,有n/2 个数比它大。
所以一种做法是利用快排的partition函数,首先选定一个数,然后用它作为pivot,使得左边的数都比它小,右边的数都比它大。然后看这个数的位置是否为n/2,如果是的话那么它就是中位数,如果大于n/2 ,就在左边找,小于n/2 ,就在右边找。

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        if(numbers.empty())
            return 0;

        int length = numbers.size();
        int start =0;
        int end = length-1;
        int index = Partition(numbers,length,start,end);
        int middle = length>>1;
        while(index!= middle){
            if(index>middle)
            {
                end = index -1; //当前的枢轴元素下大于n/2,所以搜索它的左侧
                index = Partition(numbers,length,start,end);
            }else{
                start = index + 1;
                index = Partition(numbers,length,start,end);
            }
        }
        int result= numbers[middle];

        //判断这个数的数目是否超过了数组长度的 一半
        int count=0;
        for(int i=0;i<numbers.size();++i){
            if(numbers[i]==result)
                count++;           
        }
        if(count*2<=numbers.size())
            return 0;
        return result;
    }

    int Partition(vector<int>&data,int length,int start,int end){
         int pivot = end;//最后一个元素为pivot
        int small = start-1;
        for(int i=start;i<end;++i){
            if(data[i]<data[pivot]){
                ++small;
                if(i!=pivot){
                    swap(data[small],data[i]);
                }
            }
        }
        ++small;
        swap(data[small],data[end]);
        return small;     // 返回枢轴的下标   
    } 
};

解法二
从另一个角度来想,如果一个数字出现的次数超过数组长度的一半,那么它出现的次数一定比其他所有数字出现的次数的和还要多。所以可以考虑用两个变量,一个表示当前的数字,一个表示它出现的次数,如果遍历到的数字和当前保存的数字相同,那么次数加1,否则减1,如果此处为0,就保存遍历到的这个数字,并把次数设为1.所以最后剩下的那个数字,就是出现次数超过数组长度一半的数字。

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        if(numbers.empty())
            return 0;
        if(numbers.size()==1)
            return numbers[0];
        int last = numbers[0];
        int c=1;
        for(int i=1;i<numbers.size();i++){
            if(numbers[i]==last)
                c++;
            else{
                c--;
                if(c==0){
                    last = numbers[i]; c=1;         
                }

            }              

        }
        int check =0;
        for(int i=0;i<numbers.size();i++)
            if(numbers[i]==last)
                check++;

        if(check*2>numbers.size())
            return last;
         return 0;

    }
};

第二十九题:最小的K个数

题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
解题思路
最简单的容易想到的算法,就是把输入的n个整数排序,排序之后位于最前面的k个数就是最小的k个数。这种思路的时间复杂度为O(nlogn)
解法二: O(nlogk)的算法,特别适合处理海量数据
可以用最大堆来实现,它的根结点总是大于它的子树的任意结点的值,维持堆中的结点的数目为K个,如果比最大的值还大,那么肯定不是最小的k个数,如果比最大的值小,那么可以将最大的删除,然后再重新调整堆。

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        if(input.size()<k || k <1)
            return vector<int>();           
        multiset<int ,greater<int> > smallSet;//可能有重复值
        for(int i=0;i<input.size();i++){
            if(smallSet.size()<k){
                smallSet.insert(input[i]);
                continue;
            }

            if( input[i] < *(smallSet.begin())){
                smallSet.erase(smallSet.begin());
                smallSet.insert(input[i]);
            }                        

        }  
        vector<int> result(smallSet.rbegin(),smallSet.rend());
        return result;       
    }
};

这种解法虽然慢一点,但是没有修改输入的数据,也适合海量数据处理,应该我们有可能不能把所有的数据一次性载入内存。

第三十题:连续子数组的最大和

题目描述
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。你会不会被他忽悠住?
解题思路
解法一
我们分析数组的规律,如果累积和加上b比数b本身小,为什么不从b直接开始呢?
再换言之,累积和如果已经小于0了,那么它加上b肯定是比b小的。所以我们就可以从b开始了。

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if(array.empty())
            return 0;
        int sum = 0;
        int biggestSum = (((unsigned int)~0)>>1)+1;//int 最小值
        for(int i=0;i<array.size();i++){
            if(sum<=0)
                sum = array[i];
            else
                sum+=array[i];
            if(sum>biggestSum)
                 biggestSum = sum;

        }

        return biggestSum;

    }
};

解法二
上面的流程也容易帮助我们用动态规划的思想来分析这个问题。如果用函数f(i)表示以第i个数字结尾的子数组的最大和,那么需要求出 max[f(i)]
f(i1)0 ||i==0时, f(i)=pData[i]
f(i1)>0,i0 时, f(i)=f(i1)+pData[i]
最终我们可以得到同样的结论,这是网友的一份更精简的代码。

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> a) {
      if(!a.size()) return 0;
        int mx = 0x80000000;
        for(int i = 0, s = 0; i < int(a.size()); ++i){
            s = max(s + a[i], a[i]);
            mx = max(mx, s);
        }
        return mx;
    }
};

第三十一题:数组中整数1出现的次数

题目描述
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数。
解题思路
这道题的简单思路先提一下,比较容易理解,一个数对10求余数,如果个位数字是1,那么就算包含一个1,所以用一个循环

while(n){
    if(n%10==1)
        number++;
    n=n/10;
}

就可以统计出这个数包含多少个1. 然后对1到n所有数中1的个数累加就可以了。
另一种思路是需要找规律的,在编程之美上,有对这道题的完美解读。
1位数
当n>=1时,都有f(n)=1
2位数
举个例子n=23,十位出现1的次数有10~19共10次,个位出现1的次数有1,11,21共3次;
我们可以发现个数出现1的次数不仅仅和个位数字有关,还和十位数字有关。

  • 如果个位数>=1 ,那么个数出现1的次数为十位出现1的次数+1
  • 如果个位数=0 , 那么个位出现1的次数等于十位出现1的次数。
    在十位来说,同样和个位有关。
  • 如果十位数=1, 十位出现1的次数等于个位出现1的次数+1,
  • 如果十位数>1, 十位出现1的次数等于10。

3位数
n=123
个数出现1的个数 13个: 1 ,11,21,… 91,101,111,121
十位出现1的个数20个:10~19 ,110~119
百位出现1的个数为24个: 100~123
我们可以看出个位和百位上的数字的规律和上面2位数是吻合的,个数上出现1的个数为12+1=高位数字+1
百位为1,它上面出现1的个数为23+1=低位数字+1,当然,这是因为它没有更高位了。

我们再来看更一般的情况,对于十位分析一下。因为它既有更高位,又有更低位。

  • 如果十位大于1,如n=123
    它其实是由更高位数字决定的,等于(更高位的数字+1)×10
  • 如果十位为0,那么情况会有所不同
    n=203 时,十位包含1的个数仅有:10~19,110~119 共20个,这相当于2×10=(更高位的数字)*10
  • 如果十位等于1,比如n=213
    那么十位包含1的个数有10~19 110~119 210~213 共24个,看我们列举的这些情况也可以得出,它既和高位数字有关2*10,又和低位数字有关 3+1 = 4 ,实际等于(更高位的数字)*10+低位数字+1。

这样我们终于可以得出最后的代码:

class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n)
    {             
        if(n<1)
            return 0;
        int i=1;
        int h=1,cur,tmp,low;
        int sum = 0;
        int pows[]={1,10,100,1000,10000,100000,1000000,10000000}; //lookup      
        while(h){//从个位到高位开始遍历
            h = n/pows[i];//高位
            tmp = n-h*pows[i];//除去高位
            cur = tmp/pows[i-1];//当前位

            if(cur == 0 )
                sum += h*pows[i-1];
            else if(cur > 1)
                sum += (h+1)*pows[i-1];
            else{
                low = tmp-cur*pows[i-1];
                sum += (low+1);
                sum += h*pows[i-1];   // 高位乘以当前位的权重            
            }               
            i++;
        }
        return sum;

    }
};

第三十二题:把数组排成最小的数

题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路
这道题的常规做法是先将所有的数字的全排列写出来,n个数字共有n!个排列,然后再求其中的最小值。
而我们可以找到更快速的做法。我们希望能找到一个排序规则,数组根据这个规则排序之后能得到最小的数字。要确定排序规则,给定两个数字m和n,那么我们可以拼接成mn或者nm, 我们把二者中的较小值放到前面即可。

class Solution {
public:

    string PrintMinNumber(vector<int> numbers) {
        if(numbers.empty())
            return string("");
        vector<string> strNum;
        strNum.resize(numbers.size());
        //将整数数组转换成字符串数组
        for(int i =0;i<numbers.size();i++){
            string num = num2str(numbers[i]);      
            strNum.push_back(num);
        }
        //按照既定规则进行排序
        sort(strNum.begin(),strNum.end(),smaller);
        return accumulate(strNum.begin(),strNum.end(),string(""));

    }

    static bool smaller(const string& str1,const string& str2){
        string tmp1 = str1 + str2;
        string tmp2 = str2 + str1;
        return tmp1<tmp2;           
    }

    //个人实现的一个数字转换为字符串的函数
    string num2str(int num){
        char str[20] ;
        char * p = str;
        int n = num;
        while(n){
            p++;
            n=n/10;           
        }
        *p--='\0';//指向字符串结尾
        while(num){
            *p--=num%10 + '0';
            num/=10;
        }
        //p++;
        return string(str);
    }  

    /*
    string num2str(int num){
        char str[20];
        sprintf(str,"%d",num);
        return string(str);
    }*/ 

};

如果用C语言中的qsort来实现排序,要注意一下

char * g1 = new char[20];
char*  g2 = new char[20];
...
char** strNumbers =(char**)(new int[length]);
for(int i=0;i<length;++i){
    strNumbers[i] = new char[10+1];
    sprintf(strNumbers[i],"%d",numbers[i]);
}
qsort(strNumbers,length,sizeof(char*),compare);
...
int compare(const void* strNumber1, const void * strNumber2){
    strcpy(g1,*(const char**)strNumber1);
    strcat(g1,*(const char**)strNumber2);
    strcpy(g2,*(const char**)strNumber2);
        strcat(g2,*(const char**)strNumber1);
    return strcmp(g1,g2);
}

需要注意的就是qsort的原型:

 void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));

第三十三题:丑数

题目描述
把只包含因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
解题思路
这道题的因子,应该是真因子的意思,就是说不包含1和它本身。这道题考察的是时间效率和空间效率的平衡,一种思路,我们逐个判断每个数是否是丑数,思路就是依次连续除以2,3,5 直到不能再除为止,如果最后数字为1,那么代表因数只有2,3,5。

bool isUgly(int number){
    while(number%2==0)
        number/=2;
    while(number%3==0)
        number/=3;
    while(number%5==0)
        number/=5;
    return (number==1)?true:false;
}

我们怎么用空间来换时间呢?上面的做法效率低是因为每个数我们都对它进行了计算,但是我们可以尝试找到一种只计算丑数的方法,假设数组中已有若干个丑数,那么下一个生成的丑数,一定现有的某个丑数乘以2或3或5的结果。
假设,我们把已有的最大丑数记为M,把已有的数乘以2,得到的第一个大于M的数记为 M2 , 把已有的每个数乘以3,得到的第一个最大的数记为 M3 , 相应的,…乘以5….记为 M5
由于我们不断增加M的值,相应的,得到 M2,M3,M5 的被乘数的下标也需要不断递增,没有必要每次都从头开始遍历。

class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        if(index<1)
            return 0;
        int m2=0,m3=0,m5=0;

        int last = 0;//last+1个丑数
        int * arr = new int[index]();
        arr[0] = 1;
        while(last<index-1){
             while(arr[m2]*2<=arr[last]) m2++;
             while(arr[m3]*3<=arr[last]) m3++;
             while(arr[m5]*5<=arr[last]) m5++;

             arr[++last] = min(min(arr[m2]*2,arr[m3]*3),arr[m5]*5);           

        }
        int result = arr[index-1];
        delete []arr;
        return  result;
    }
};

这种思路不需要在非丑数的整数上作任何计算,但是牺牲了空间,因此要存已有的丑数,还有一些对应的下标,属于空间换时间的做法。

第三十四题:第一个只出现一次的字符位置

题目描述
在一个字符串(1<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符的位置。若为空串,返回-1。位置索引从0开始
解题思路
题目描述与字符出现的次数有关系,自然而然就容易想到哈希表。
字符占据一个字节,所以共是256种可能,可以用一个256的数组来作为哈希表,用字符的
ASCII码值作为下标,存储每个字符出现的次数。

class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        if(str.empty())
            return -1;
        int hashtable[100]={0}; 
        for(int i=0;i<str.size();i++){           
            hashtable[str[i]-'A']++;
        }
        for(int i=0;i<str.size();i++){           
            if(hashtable[str[i]-'A']==1)
                return i;
        }
        return -1;       
    }
};

第三十五题:数组中的逆序对

题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
解题思路
先把数组分隔成子数组,先统计子数组内部逆序对的数目,然后再统计出两个相邻子数组之间逆序对的数目。这是非常经典的用归并排序可以求解的问题。
下面是用自底向上的归并排序写出的求逆序对的算法实现。

class Solution {
public:
    int InversePairs(vector<int> data) {
            if(data.empty())
                return 0;
            vector<int> tmp;    
            tmp.resize(data.size());
            int seg = 1;
            int inversePair = 0;
            while(seg<data.size()){
                mergePass(data,tmp,seg,inversePair);
                seg+=seg;              
                mergePass(tmp,data,seg,inversePair);
                seg+=seg;
            }
            return inversePair;      
    }

    void merge(vector<int >&src, vector<int>&dst, int start, int mid, int end,int& inversePair) {

        int begin1 = start;
        int end1 = mid;
        int begin2 = mid + 1;
        int end2 = end;
        int k = start;
        while (k <= end) {
            if (begin1 > end1)
                dst[k++] = src[begin2++];       
            else if (begin2 > end2)
                dst[k++] = src[begin1++];
            else {
                if (src[begin1] <= src[begin2])
                    dst[k++] = src[begin1++];
                else{
                    dst[k++] = src[begin2++];
                    inversePair+= (end1-begin1+1);//表示后半段比前半段的所有值都小
                }
            }
        }
    }

    void mergePass(vector<int> &src,vector<int>& dst,int seg,int&inversePair){
        int start_ind = 0;
        int size = src.size();
        while(start_ind<=size-2*seg){
            merge(src,dst,start_ind,start_ind+seg-1,start_ind+2*seg-1,inversePair);
            start_ind+=2*seg;
        }
        if(start_ind+seg<size)
            merge(src,dst,start_ind,start_ind+seg-1,size-1,inversePair);
        else
            for(int i=start_ind;i<size;i++)
                dst[i] = src[i];
    }     
};

第三十六题: 两个链表的第一个公共结点

题目描述
输入两个链表,找出它们的第一个公共结点。
解题思路
使用蛮力法并不是好方法,因为该方法的时间复杂度为 O(mn)
但是我们换种思考方法,如果两个链表有公共结点,从某个结点开始,它们的next都指向同一结点。所是一个Y字型。所以尾部必然是公共结点。
尾部必须从前往后遍历,但是却要最先进行比较。所以我们可以想到栈的特点。分别把两个链表的结点放入两个栈里,这样两个链表的尾结点就位于两个栈的栈顶,接下来比较两个栈顶的结点是否相同,如果相同,就把栈顶弹出接着比较下一个栈顶,最后一个相同的结点就是两个链表的第一个公共结点。
这种算法虽然好,但是需要用到辅助的栈,空间复杂度也是 O(m+n) 。换种思路,我们希望同时到达末尾结点,但是由于链表长度不一样无法直接实现,但是如果我们知道一个链表比另一个长多少,又知道末尾是对齐的,让长链表的指针先走几步,那么当它们结点相同时,可以判定是公共结点。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
            val(x), next(NULL) {
    }
};*/
class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2) {

        int len1 = getLength(pHead1);//得到链表的长度
        int len2 = getLength(pHead2);
        if(len1==0||len2==0)
            return NULL;
        ListNode* pLong = pHead1; ListNode* pShort = pHead2;
        int dif = len1-len2;       
        if(len1<len2){
            pLong = pHead2;
            pShort = pHead1;
            dif = len2-len1;           
        }
         //先在长链表上走几步,再同时在两个链表上遍历。
        for(int i=0;i<dif;i++){
            pLong=pLong->next;           
        }

        while(pLong&&pShort&& pLong!=pShort){
            pLong =pLong->next;
            pShort =pShort->next;

        }

        return pLong;       

    }   

    int getLength(ListNode* p1){
        if(!p1)
            return 0;
        int len=0;
        while(p1){
            len++;
            p1=p1->next;           
        }
        return len;

    }
};

第三十七题:数字在排序数组中出现的次数

题目描述
统计一个数字在排序数组中出现的次数。
解题思路
利用二分查找,比如统计数字k出现的次数,那么我们找到重复出现的数字的第一个k和最后一个k。
先看下剑指offer上基于递归的实现吧。

int GetFirstK(int * data, int length, int k, int start,int end){
    if(start>end)
        return -1;
    int middleIndex = (start+end)/2;
    int middleData = data[middleIndex];
    if(middleData == k){
           if((middleIndex>0 && data[middleIndex-1]!=k)||middleIndex ==0)//前面的数不为k,说明是第一个k
               return middleIndex;
           else
               end = middleIndex -1;
    }
    else if(middleData>k)
            end = middleIndex-1;
    else
            start = middleIndex+1;
   return  GetFirstK(data,length,k,start,end);          

 }

最后一个k,也可以用递归的方式写出来。

int GetLastK(int* data, int length, int k,int start,int end){
    if(start>end)
        return -1;
    int middleIndex = (start+end)/2;
    int middleData = data[middleIndex];
    if(middleData == k){
        if((middleIndex < length-1 && data[middleIndex+1]!=k)|| middleIndex == length -1)
            return middleIndex;
        else 
            start = middleIndex + 1;
    }
    else if(middleData<k)
        start = middleIndex + 1;
    else
        end = middleIndex -1;

    return GetLastK(data,length,k,start,end);
}

计算k在数组中出现的次数:

int GetNumberOfK(int * data,int length,int k){
    int number=0;
    if(data!=NULL && length>0){
        int first = GetFirstK(data,length,k,0,length-1);
        int last = GetLastK(data,length,k,0,length-1);
        if(first>-1&& last>-1)
            number = last - first +1;   
    }
}

用非递归的方法写,可能需要一些助记技巧。

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {

        if(data.empty())
            return 0;

        int i=0;
        int j=data.size()-1;
        int smallIndex=0;
        //找最前面的k
        while(i<j){

            int mid = i+((j-i)>>1);
            if(k<=data[mid])
                j = mid;
            else
                i = mid+1;         

        }
        if(data[i]==k)
            smallIndex = i;
        else
            smallIndex = -1;
         //找最后的k
        i = 0;j=data.size()-1;

        while(i<j-1){
            int mid = i+((j-i)>>1);
            if(k>=data[mid])
                i = mid;
            else
                j = mid;

        }
        int bigIndex = j;
        if(data[j]==k)
            bigIndex = j;
        else if(data[i]==k)
            bigIndex = i;
        else
            bigIndex = -1;
        if(bigIndex-smallIndex>=0&&bigIndex!=-1)
            return bigIndex-smallIndex+1;       
        return 0;
    }         
};

第三十八题:二叉树的深度

题目描述
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
解题思路
如果这棵树只有一个结点,那么其深度为1 ,如果只有左子树没有右子树,其深度为左子树的深度+1,如果只有右子树,那么其值为右子树的深度+1,如果既有左子树又有右子树,那么树的深度是两棵子树深度的较大值+1。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};*/
class Solution {
public:
    int TreeDepth(TreeNode* pRoot)
    {
        if(!pRoot)
           return 0;
        int left = TreeDepth(pRoot->left);
        int right = TreeDepth(pRoot->right);
        return (left>=right)? left+1:right+1;  //?:优先级较低


    }
};

第三十九题:平衡二叉树

题目描述
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
解题思路
之前我们知道怎么求二叉树的深度,如果在遍历树的每个结点时,调用函数TreeDepth得到它的左右子树的深度不超过1,那么就是平衡二叉树。如果直接这么计算,那么有些结点会重复遍历。
用后序遍历的方式,在遍历每个结点的时候记录它的深度,传给上层结点就可以了。

//后续遍历二叉树,遍历过程中求子树高度,判断是否平衡
class Solution {
public:
    bool IsBalanced(TreeNode *root, int & dep){
        if(root == NULL){
            return true;
        }
        int left = 0;
        int right = 0;
        if(IsBalanced(root->left,left) && IsBalanced(root->right, right)){
            int dif = left - right;
            if(dif<-1 || dif >1)
                return false;
            dep = (left > right ? left : right) + 1;
            return true;
        }
        return false;
    }
    bool IsBalanced_Solution(TreeNode* pRoot) {
        int dep = 0;
        return IsBalanced(pRoot, dep);
    }
};

第四十题:数组中只出现一次的数字

题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
要求时间复杂度为O(n),空间复杂度为O(1)。
解题思路
例如输入数组{2,4,3,6,3,2,5,5} ,输出4和6,因为只有4和6这两个数字只出现一次。
考虑只出现2次的数字意味着什么,假设数组中只有一个数字只出现了一次,别的都出现了两次。这就是说我们从头到尾异或数组中的每一个数字,那么最终结果刚好是那个只出现一次的数字。
再回来原始的问题,如果我们把原数组分为两个子数组,使得每个子数组包含一个只出现一次的数字,而其他数字都成对出现两次。就可以按照上面的方法分别找出只出现一次的数字。
对任何相同的数字做相同的运算肯定都会分到同一组,我们不用担心把它们分开的问题。对于两个不同的数字,它们异或的结果一定不为0,也就是二进制表示中一定有为1的这一位,就是说a和b的二进制对应的这位是0还是1来将它们分开。

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        *num1 =0;*num2=0;
        if(data.size()<2){          
            return ;
        }

        int xorResult =0;
        for(int i = 0;i<data.size();++i){
            xorResult ^= data[i];
        }
        int indexBix = 0;
        //from right to left
        while(((xorResult&1)==0) &&(indexBix<8*sizeof(int))){
            xorResult>>=1; ++indexBix;           
        }

        for(int i=0;i<data.size();++i){
            if(isBit1(data[i],indexBix))
                *num1^=data[i];
            else
                *num2^=data[i];           
        }
        return;

    }

    bool isBit1(int num,int index){
        num = num>>index; return (num&1);
    }

};

第四十一题:和为S的连续正数序列

题目描述
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
输出描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

解题思路
我们分别用small和big表示序列的最小值和最大值,相当于我们滑动一个窗口,它的左界和右界。small初始化为1,big初始化为2,如果从small到big的序列的和大于s,就从序列中减去较小的值,也就是增大small的值,如果和小于s,就增大big,让这个序列包含更多的值。

class Solution {
public:
    vector<vector<int> > FindContinuousSequence(int sum) {
        vector<vector<int> > result;

        if(sum<3)//因为最小的数s至少要包含两个数,1+2=3
            return result;

        int i = 1; int j=2;
        int middle = (sum+1)>>1; int currentSum =i+j;
        while(i<middle){//左界不可能超过中s的一半middle,因为middle右侧的数都比s/2大而这是不可能的(两个就超过s了)

            while(currentSum<sum){
                j++;
                currentSum+=j;
            }
            if(currentSum==sum){
                vector<int> r;
                for(int k=i;k<=j;k++)
                  r.push_back(k);
                result.push_back(r);
            }                
            currentSum-=i++;
        }       
        return result;   
    }

};

第四十二题:和为S的两个数字

题目描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
输出描述:
对应每个测试案例,输出两个数,小的先输出。
解题思路
定义两个指针,一个指针指向数组第一个,第二个指针指向数组最后一个数字。如果这两个数字的和大于s,把第二个指针向前移动一个数字,如果这时又小于s,就把第一个指针向后移动一个数字。这种算法的时间复杂度为 O(n)

class Solution {
public:
    vector<int> FindNumbersWithSum(vector<int> array,int sum) {
        vector<int> result;
        if(array.empty())
            return result;
        int size = array.size();
        int i = 0;
        int j = size-1;
        while(i<j){
            int currentSum=array[i]+array[j];
            if(currentSum==sum)
            {
                if(array[i]<array[j])
                {
                    result.push_back(array[i]);
                    result.push_back(array[j]);

                }
                else{
                    result.push_back(array[j]);
                    result.push_back(array[i]);
                }
                return result;

            }     
            if(currentSum>sum)
                j--;
            else
                i++;           
        }
        return result;
    }
};

第四十三题:左旋转字符串

题目描述
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
解题思路
以abcdefg为例,如果我们左旋转2位,可以把这个字符串分为两组,一组是ab,一组是cdefg。分别翻转这两个部分,得到bagfedc,接下来再翻转整个字符串,可以得到cdefgab。

class Solution {
public:
    string LeftRotateString(string str, int n) {
        if(str.empty()||n<=0)
            return str;

        reverse(str.begin(),str.begin()+n);
        reverse(str.begin()+n,str.end());
        reverse(str.begin(),str.end());
        return str;
    }   

};

第四十四题: 翻转单词顺序列

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

先对整个字符串进行翻转,再对单词逐个翻转。

class Solution {
public:
    string ReverseSentence(string str) {
        if(str.empty())
            return str;
        reverse(str.begin(), str.end());
        int left  = 0;      
        int i =0;
        while(str[i]!='\0'){
            //遍历直到出现空格
            while(str[i]!='\0'&&str[i]!=' '){               
                 ++i; 
            }           
            if(i>left+1)
                reverse(str.begin()+left,str.begin()+i);
            //跳过空格,直到下个单词的左边界
            while(str[i]!='\0'&&str[i]==' '){
                ++i;
            }   
            left = i;           
        }
        return str;                      
        }
 };
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值