剑指offer之编程是一种习惯

矩阵中的路径

class Solution {
public:
    bool hasPath(char* matrix, int rows, int cols, char* str) {
        if(rows <= 0|| cols <= 0 || str == NULL || matrix == NULL) {
            return false;
        }
        bool* visited = new bool[rows*cols];
        memset(visited, false, rows*cols);
        for(int i = 0; i < rows; i++) {
            for(int j = 0; j < cols; j++) {
                if(helper(matrix, rows, cols, i, j, str, visited)) {
                    delete[] visited;
                    return true;
                } // else find next start of match
            }
        }
        delete[] visited;
        return false;
    }
    bool helper(char* matrix, int rows, int cols, int x, int y, char* str, bool* visited) {
        if(*str == '\0') {
            return true;
        }
        if(x < 0 || x >= rows || y < 0 || y >= cols || visited[x*cols+y]) {
            return false;
        }
        bool next = false;
        if(matrix[x*cols+y] == *str) { // match
            visited[x*cols+y] = true; // visit
            next = helper(matrix, rows, cols, x-1, y, str+1, visited) ||
                   helper(matrix, rows, cols, x+1, y, str+1, visited) ||
                   helper(matrix, rows, cols, x, y-1, str+1, visited) ||
                   helper(matrix, rows, cols, x, y+1, str+1, visited);
            if(!next) { // all possibilities(up,down,left,right) failed
                visited[x*cols+y] = false; // invalid match,trace-back
            }
        }
        return next;
    }
};

 

【44】翻转单词顺序列

这题学一下字符串的分割,C语言的strtok(). 相当于还是通过遍历字符串来实现的。

class Solution {
public:
    string ReverseSentence(string str) {
        if(str.length() == 0) return "";
        if(str == " ") return " ";
        const char *sep = " ";
        char *p;
        vector words;
        string res = "";
        char *c_str = (char *)str.c_str();
        p = strtok(c_str, sep);
        while(p){
            words.push_back(p);
            p = strtok(NULL, sep);
        }
        for(auto x : words){
            res = " " + x + res;
        }
        if(res.size() > 0) res = res.substr(1);
        return res;
    }
};

【43】左旋转字符串

这题学一下C++字符串的截取。

class Solution {
public:
    string LeftRotateString(string str, int n) {
        int len = str.length();
        if(!len || len < n) return "";
        string tmp = str.substr(0, n); //从0开始,nge
        string sstr = str.substr(n); //从n开始,到字符串末尾
        return sstr + tmp;
    }
};

【41】和为S的连续正数序列

输出描述: 输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序。

这题要借鉴上题的思路,即从两头找序列的头和尾。但这题还有两个难点:

1: 头尾指针的初始化

找和为指定值的序列,初始化头尾指针为1和2.

2: 循环跳出的条件

因为序列至少要2个元素,如果是两个元素的话,那二者相邻,所以头指针加到(sum + 1)/2就结束了。

class Solution {
public:
    int sumAll(int start, int end){
        int sum = 0;
        while(start <= end){
            sum += start;
            start++;
        }
        return sum;
    }
    vector > FindContinuousSequence(int sum) {
        int front = 1, end = 2;
        vector >res;
        while(front < (sum + 1)/2){
            int tmp = sumAll(front, end);
            if(tmp < sum){
                end++;
            }else if(tmp > sum){
                front++;
            }else{
                vector part;
                for(int i = front; i <= end; i++){
                    part.push_back(i);
                }
                res.push_back(part);
                end++;
            }
        }
        return res;
    }
};

【42】和为S的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

定义两个伪指针,从数组两头找,算头和尾的加和,如果比目标大,则前移尾指针;如果比目标小,则后移头指针。

思路太重要了,要积累。

class Solution {
public:
     vector FindNumbersWithSum(vector array,int sum) {
        int front = 0, end = array.size() - 1, multi = INT_MAX;
        vector res;
        while(front < end){
            if(array[front] + array[end] > sum){
                end--;
            }else if(array[front] + array[end] < sum){
                front++;
            }else{
                if(array[front] * array[end] < multi){
                    multi = array[front] * array[end];
                    res.push_back(array[front]);
                    res.push_back(array[end]);
                }
                front++;
                end--;
            }
        }
         return res;
     }
};

【40】数组中只出现一次的数字

一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。

位运算之需要mark的技能:异或去重

根据异或的运算规则,两相同的数异或结果位0,即任意偶数个数异或结果为0,由于异或运算满足交换律,这些数不需要相邻,只要总数是偶数个,全异或结果就为0;如果有一个出现奇数次的数,那么全异或的结果就是这个数。

现在问题来了,题目让我们找两个出现奇数次的数字,该怎么做呢?

现在做全异或得到的结果肯定是这两个数的异或结果,肯定不为0,即结果肯定有一个位为1(假设是第n位);通过n我们就能把这两个数分到两个不同的子集中:这两个数第n位一个为0,一个为1. 对这两个子集分别做全异或,我们就找出了出现奇数次的两个数。而划分两个子集的依据也很明显:对于一个整数,第n位要么为1,要么为0.

关于第n位:剑指ofo上面说是第一位,但讲道理应该是任意一个为1的位来划分都可以,只要把两个出现奇数次的分到不同子集中就行了 。

class Solution {
public:
    int findFirstBitIs1(int num){
        if(num == 0) return 0;
        int index = 0;
        while((num & 1) == 0){
            index++;
            num >>= 1;
        }
        return index;
    }
    int xorAll(vector data){
        int num = 0;
        for(auto x: data){
            num ^= x;
        }
        return num;
    }
    void FindNumsAppearOnce(vector data,int* num1,int *num2) {
        int num = xorAll(data);
        int indexOf1 = findFirstBitIs1(num);
        int separator = 1;
        while(indexOf1--){
            separator <<= 1;
        }
        vector left; vector right;
        for(auto x : data){
            if(x & separator){
                right.push_back(x);
            }else{
                left.push_back(x);
            }
        }
        *num1 = xorAll(left);
        *num2 = xorAll(right);
    }
};

【39】平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

回顾平衡二叉树的定义:

1: 空树是平衡二叉树;

2: 如果是非空树,要求左子树和右子树都是平衡二叉树,且左右子树高度差不能超过1;

可以看出平衡二叉树是一个递归的定义。分别求出左右子树的高度,然后比较就可以了。这道题与上一题循序渐进。

【特别注意:只满足左右子树高度差是不可以的,要求左右子树也都是平衡二叉树!!】

class Solution {
public:
    bool isBalanced(TreeNode* root) {
        if(!root)   return true;
        int leftLength = getHeight(root->left);
        int rightLength = getHeight(root->right);
        return (abs(leftLength - rightLength) <= 1) && isBalanced(root->left) && isBalanced(root->right); // CAREFULLY!!
    }
    int getHeight(TreeNode* root) {
        if(!root)   return 0;
        return max(getHeight(root->left), getHeight(root->right)) + 1;
    }
};

【38】二叉树的深度

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

递归的代码真是又好写,又不好写,一旦什么地方写错了,很难调试=。=

思路是这样的:空树深度为0;如果只有一个根结点,深度为1;如果根有左子树而无右子树,深度为左子树深度+1;如果根有右子树而无左子树,深度为右子树深度+1;如果都有,则深度为左右子树深度较大者 + 1. +1实际上是对根结点对访问。

因为是递归代码,不用单独对子树判空,如果子树为空,会在第一行返回0.

class Solution {
public:
    int TreeDepth(TreeNode* pRoot){
        if(!pRoot) return 0;
        int ldepth = TreeDepth(pRoot->left);
        int rdepth = TreeDepth(pRoot->right);
        return max(ldepth, rdepth) + 1; // visit root
    }
};

【37】数字在排序数组中出现的次数

统计一个数字在排序数组中出现的次数。

很迷==心累==见代码。。

版本一

class Solution {
public:
    int GetFirstK(vectordata, int k, int start, int end){
    if(start > end) return -1;
    while(start <= end){
        int mid = start + (end - start)/2;
        if(data[mid] == k){
            if(mid == 0 || data[mid - 1] != data[mid])
                return mid;
            if(data[mid - 1] == data[mid])
                end = mid - 1; //敲黑板
        }else if(data[mid] > k){
            end = mid - 1; //敲黑板
        }else{
            start = mid + 1;
        }
    }
    return -1;
}

int GetLastK(vector data, int k, int start, int end){
    if(start > end) return -1;
    while(start <= end){
        int mid = start + (end - start)/2;
        if(data[mid] == k){
            if(mid == data.size() - 1 || data[mid + 1] != data[mid])
                return mid;
            if(data[mid + 1] == data[mid])
                start = mid + 1;
        }else if(data[mid] > k){
            end = mid - 1; //敲黑板
        }else{
            start = mid + 1;
        }
    }
    return -1;
}

int GetNumberOfK(vector data ,int k) {
    int len = data.size();
    if(!len) return 0;
    int first = GetFirstK(data, k, 0, len - 1);
    int last = GetLastK(data, k, 0, len - 1);
    if((first == -1 && last == -1) || first > last)
        return 0;
    return last - first + 1;
}
};

版本二:

class Solution {
public:
    int BinarySearch(vectordata, int k, int start, int end){
    while(start <= end){
        int mid = start + (end - start)/2;
        if(data[mid] == k){
            return mid;
        }else if(data[mid] > k){
            end = mid - 1;
        }else{
            start = mid + 1;
        }
    }
    return -1;
}

int GetNumberOfK(vector data ,int k) {
    int len = data.size();
    if(!len) return 0;
    int index = BinarySearch(data, k, 0, len - 1);
    if(index == -1) return 0;
    int first = index - 1, last = index + 1, count = 1;
    while(first >= 0 && data[first] == k){
        first--; count++;
    }
    while(last < len && data[last] == k){
        last++; count++;
    }
    return count;
}
};

【36】两个链表的第一个公共结点

输入两个链表,找出它们的第一个公共结点。

最直接的方法就是暴力法,复杂度为O(n^2),这肯定不是好的算法。

其实我们想想,为什么会O(n^2)?原因就是两个链表是不一样长的,也就是说第一个共同结点之前,结点数目是不一样的,所以才要暴力搜索。但是从第一个共同结点之后,后面的结点必然是相同的,因为单链表结点只有一个next指针。这个point看似简单,实则非常重要。所以重叠子链的长度相同,我们只需要先求出两个链表各自的长度,然后让较长的链表的遍历指针跨过长出来的结点,以后两个遍历指针就可以同步移动了,因而也降低了时间复杂度。

class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        if(!pHead1 || !pHead2) return NULL;
        int len1 = 0, len2 = 0;
        ListNode* pNode1 = pHead1;
        ListNode* pNode2 = pHead2;
        while(pNode1){
            len1++;
            pNode1 = pNode1->next;
        }
        while(pNode2){
            len2++;
            pNode2 = pNode2->next;
        }
        int more;
        pNode1 = pHead1;
        pNode2 = pHead2;
        if(len1 > len2){
            more = len1 - len2;
            while(more--)
                pNode1 = pNode1->next;
        }
        if(len1 < len2){
            more = len2 - len1;
            while(more--)
                pNode2 = pNode2->next;
        }
        while(pNode1 != pNode2){
            pNode1 = pNode1->next;
            pNode2 = pNode2->next;
        }
        return pNode1;
    }
};

【35】数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。

归并排序的思想,只不过在两个子序列的元素比较时,如果前者大于后者,说明出现逆序对,又因为子序列是递增有序的,第一个序列剩下的元素都构成逆序对,共有mid - i + 1 个。

如果对归并不太熟悉,可以参考我对另一篇博客,以及其他资料,搞清楚了再来做这个题,爽的一p。

这个故事告诉我们,打好基础是多么的重要。

class Solution {
public:
    int InversePairs(vector data) {
        if(!data.size()) return 0;
        long long count = 0;
        MergeSort(data, 0, data.size()-1, count);
        return count % 1000000007;
    }
void MergeSort(vector &arr, int low, int high, long long &count){
    if(low < high){
        int mid = low + (high - low) / 2;
        MergeSort(arr, low, mid, count); //划分: do nothing
        MergeSort(arr, mid + 1, high, count);
        Merge(arr, low, mid, high, count); //合并
    }  
    return;
}
    
void Merge(vector &arr, int low, int mid, int high, long long &count){
    int i = low, j = mid + 1, k = 0;
    vector tmp(high - low + 1, 0);
 
    while(i <= mid && j <= high){
        if(arr[i] < arr[j]){
            tmp[k++] = arr[i++];
        }else{
            tmp[k++] = arr[j++];
            count += mid - i + 1;
        }
    }
    while(i <= mid){
        tmp[k++] = arr[i++];
    }
    while(j <= high){
        tmp[k++] = arr[j++];
    }
    for(int x = 0; x < tmp.size(); x++)
        arr[low + x] = tmp[x];
}

};

【34】第一个只出现一次的字符

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)

解这道题的牛逼之处就在于C语言的数组,tm怎么可以用index初始化,然后用字符去索引。。

然后C语言的编程习惯,搞一个tableSize,开辟一段足够大的空间,就避免了对string中字符去重的麻烦,但会浪费空间,这是小事,就感觉有点low就是。。我从网上down了一个对string中字符去重对代码,但他也搞了一个tableSize。。没有什么区别我就没用。代码还挺有意思的,快慢指针,附在后面。

然后就是tableSize定义为const变量要比定义为宏要好。

class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        if(!str.length()) return -1;
        const int tableSize = 256;
        int hashTable[tableSize];
        for(int i = 0; i

代码来自:https://blog.csdn.net/wordwarwordwar/article/details/39962183

char* DeleteRepeatCharacters(char* str){
    if(str==NULL)
        return NULL;
    const int hashsize=256;
    bool hashtable[hashsize];
    for(int i=0;i

扩展:剑指ofo电子版P196 重要!要看哦!

1: 在第一个字符串中删除第二个字符串中的字符

2: 去除字符串中重复的字符

3: 变位词判断

【33】丑数

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

写出前几个丑数来找思路:

1 2 3 4 5 6 8 9 10 12...

我们发现后面的丑数都是前面某个丑数*2或*3或*5得到的。理解这一点非常重要。

从当前丑数出发,寻找下一个丑数:下一个丑数一定来自于当前丑数及其之前丑数*2 *3 *5之后的结果,并且“刚刚”大于当前丑数,也就是大于当前丑数中最小的一个;每一轮更新当前丑数,直到求出第N个。

C++ API学习:min_element返回最小元素的迭代器指针,取元素需要用指针符号。

class Solution {
public:
int GetUglyNumber_Solution(int index) {
    if(index == 0) return 0;
    vector uglyArray;
    vector uglySpace;
    int current = 1;
    uglyArray.push_back(current);
    while(uglyArray.size() < index){
        for(auto x : uglyArray){
            if(x * 2 > current)
                uglySpace.push_back(x * 2);
            if(x * 3 > current)
                uglySpace.push_back(x * 3);
            if(x * 5 > current)
                uglySpace.push_back(x * 5);
        }
        auto nextUgly = *min_element(uglySpace.begin(), uglySpace.end());
        uglyArray.push_back(nextUgly);
        current = nextUgly;
        uglySpace.clear();
    }
    return uglyArray.back();
}
};

【32】把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

心路历程:

拿到题目真是连个可行的思路都没有,看了剑指ofo上面的讲解后,产生了第一重认识:这个题就是找数字两两之间排列的关系,取排列后组成的数较小者;实际上是自定义一个排序规则,对目标数据进行排序:组成数较小的,称 前面的数 小于 后面的数。

这个思路可以说非常奇妙了,于是我想搞一个二维数组,保存数字两两之间的“大小”关系。。但是代码写起来非常难受。。于是去看了牛客网这个题的排行榜,然后发现有一个非常牛逼的东西叫做 C++的sort!!可以自定义排序,简直就是为这个题量身定做的!

不得不感叹,大佬还是大佬,还是要多向大佬学啊

加油^o^

class Solution {
public:
    static bool cmp(int a, int b){
    string A = to_string(a) + to_string(b);
    string B = to_string(b) + to_string(a);
    return A < B;
}

string PrintMinNumber(vector numbers) {
    if(!numbers.size()) 
        return "";
    std::sort(numbers.begin(), numbers.end(), cmp); //根据自定义规则进行排序
    string res = "";
    for(auto x : numbers){
        res += to_string(x); //排好序后进行拼接
    }
    return res;
}
};

【31】整不明白

【30】连续子数组的最大和

在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和。(子向量的长度至少是1)

遍历数组,依次求出部分和(tmp),但当部分和比当前数小时,说明部分和是负数,即之前当部分和小于从当前数开始往后当最大和,则抛弃这个部分和;从当前数开始,重新求部分和。遍历的过程不断更新最大和。

代码很简单,理清思路很重要。

class Solution {
public:
    int FindGreatestSumOfSubArray(vector array) {
        int maxSum = INT_MIN, tmp = 0;
        for(auto x : array){
            tmp += x;
            if(tmp < x)
                tmp = x;
            if(tmp > maxSum)
                maxSum = tmp;
        }
        return maxSum;
    }
};

【29】没啥意思

【28】数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

1: 数组去重

2: 建立字典

3: 遍历数组,统计出现次数

4: 遍历字典,寻找target

class Solution {
public:
    int MoreThanHalfNum_Solution(vector numbers) {
        vector m_numbers(numbers.begin(), numbers.end());
        sort(m_numbers.begin(), m_numbers.end());
        vector::iterator unique_number = unique(m_numbers.begin(), m_numbers.end());
        m_numbers.erase(unique_number, m_numbers.end());
        
        map timesMap;
        for(auto x : m_numbers){
            timesMap[x] = 0;
        }

        for(auto x : numbers){
            timesMap[x]++;
        }
        int len = numbers.size();
        int target = 0;
        map::iterator iter = timesMap.begin();
        while(iter != timesMap.end()){
            if(iter->second > len/2)
                target = iter->first;
            iter++;
        }
        return target;
}
};

【27】字符串的排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

1: 递归思路的理解:固定第一个字符,求剩余子串的排列;固定子串中第一个字符,求剩余子子串的排列;

2: 递归出口的理解:start指针遍历完字符串,才生成一个结果

遇到的小困难:

1: 递归程序不知道何时return:用参数传递结果

2: 第一个字符需要与剩余子串依次交换,所以一次递归结束后,需要交换回来,见注释

class Solution {
public:
    void Permutation(string str, int start, vector& res){
    if(start >= str.length())
        res.push_back(str);
    for(int i = start; i Permutation(string str) {
        vector res;
        if(!str.length())
            return res;
        Permutation(str, 0, res);
        sort(res.begin(), res.end());
        return res;
    }
};

【26】

【25】复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。

分三步:

1: 复制基本链表

2: 复制random域

3: 重连链表

注意空指针的判断

class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead)
    {
        if(!pHead)
            return NULL;
        CloneNodes(pHead);
        CloneRandomNodes(pHead);
        return ReConnectListNode(pHead);
    }
    void CloneNodes(RandomListNode* pHead){
        RandomListNode* pNode = pHead;
        while(pNode){
            RandomListNode* newNode = new RandomListNode(pNode->label);
            newNode->next = pNode->next;
            pNode->next = newNode;
            pNode = newNode->next;
        }
    }
    void CloneRandomNodes(RandomListNode* pHead){
        RandomListNode* pNode = pHead;
        while(pNode && pNode->next){
            if(pNode->random){
                pNode->next->random = pNode->random->next;
            }
            pNode = pNode->next->next;//一次跨两步
        }
    }
    RandomListNode* ReConnectListNode(RandomListNode* pHead){
        RandomListNode* newHead = pHead->next;
        RandomListNode* pNode = pHead;
        RandomListNode* nodeNext;//nodeNext指向pNode的后继结点
        while(pNode->next){
            nodeNext = pNode->next;
            pNode->next = nodeNext->next;
            pNode = nodeNext;
        }
        return newHead;
    }
};

【24】二叉树中和为某一值的路径

输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)

画出一棵树,手动试一下就能发现规律。路径定义为从根结点到叶结点的一条结点序列,这个题让我们“凑”路径上所有结点和为某一个整数值。显然,每遍历一个结点,就“凑”了一部分值,remian为还需凑多少;若遍历到叶结点时,remain恰好为0,说明找到一条路径。因为路径从根结点开始,所以三种遍历方式中选择前序遍历。

因为可能存在多条路径,所以每次递归返回父结点的时候,应该删去已访问过的结点,并加回remain值,继续寻找路径。

总结:这次刷的时候才发现,关于二叉树的题很多都是基于树的三种遍历的,这道题也是,如注释所示,前序遍历模型中visit根结点的操作变形为push_back;然后visit左子树和visit右子树与前序遍历模型几乎是一致的;只是遍历到叶结点时需要做一些判断和额外操作。所以本题的难点有两个:

1:发现需要前序遍历这件事;

2:找到处理路径和的办法;

第一点更难。

class Solution {
public:
    vector > res;
    vector path;
    vector > FindPath(TreeNode* root,int expectNumber) {
        if(!root)
            return res;
        path.push_back(root->val);//visit根
        int remain=expectNumber-root->val;
        bool isLeaf=(!root->left) && (!root->right);
        if(isLeaf && remain==0){
            res.push_back(path);
        }
        FindPath(root->left,remain);//visit左子树
        FindPath(root->right,remain);//visit右子树
        path.pop_back();
        remain+=root->val;
        return res;

    }
};

【23】

【22】从上往下打印二叉树

二叉树的层序遍历,使用辅助队列,思路非常清晰。

class Solution {
public:
    vector PrintFromTopToBottom(TreeNode* root) {
        vectorres;
        queue m_queue;
        if(!root)
            return res;
        m_queue.push(root);
        while(!m_queue.empty()){
            TreeNode* topNode=m_queue.front();
            if(topNode->left)
                m_queue.push(topNode->left);
            if(topNode->right)
                m_queue.push(topNode->right);
            res.push_back(topNode->val);
            m_queue.pop();
        }
        return res;
    }
};

【21】栈的压入、弹出序列

根据给定的压入序列判断弹出序列是不是合法的。

这是近期做的一个很额熏的题目,手工判断非常简单,但是很难找到规律。

最直接的思路是:根据要判断的弹出序列,pop一遍,就知道是不是合法的了。

首先在压入序列中找到弹出序列的第一个元素(目标元素),设计一个辅助栈,压入序列中在此之前的元素全部入栈;如果在压入序列中找到了目标元素,则辅助栈可以出栈,目标元素变成弹出序列中第二个元素,依次类推;否则说明把压入序列中所有元素都入栈了,仍然没有找到目标元素,说明弹出序列是不合法的,直接退出。

最终如果弹出序列全部访问并且辅助栈中元素全部弹出,说明我们尝试pop弹出序列成功,否则说明尝试失败。

需要注意:边界条件容错:空数组情况;入栈访问数组不能超过数组边界;

理清楚逻辑非常重要。

class Solution {
public:
    bool IsPopOrder(vector pushV,vector popV) {
        bool res=false;
        int nextPop=0, nextPush=0;
        stack m_stack;
        if(pushV.size()>0 && popV.size()>0){
            while(nextPop < popV.size()){
                while(m_stack.empty() || m_stack.top() != popV[nextPop]){
                    if(nextPush >= pushV.size())
                        break;
                    m_stack.push(pushV[nextPush]);
                    nextPush++;
                }
                if(m_stack.top() == popV[nextPop]){
                    m_stack.pop();
                    nextPop++;
                }else
                    break;//注意
                
            }
            if(m_stack.empty() && nextPop == popV.size())
                res=true;
        }
        return res;
    }
};

【20】具有min功能的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

既然说了定义栈的数据结构,意思就是实现min我们只有两个工具:push()和pop(). 由于栈的内容是动态变化的,所以增加一个min属性解决不了问题;这个题不仅要实现min,并且要O(1),我们想到栈的top()是O(1)的,因而可以设计一个min_stack保存当前的最小元素,min_stack.top()就可以拿到最小元素;一旦有新的更小元素,就push;如果当前最小元素被从数据栈pop()出去了,那么min_stack也pop(). (data_stack是正常的数据栈)

class Solution {
private:
    stack data_stack, min_stack;
public:
    void push(int value) {
        data_stack.push(value);
        //if(min_stack.empty()){
        //    min_stack.push(value);
        //}else if(value

【19】

【18】二叉树的镜像

把前序遍历二叉树的经典算法中,访问结点的操作-变成交换左右子结点的操作,直到遍历结束。

class Solution {
public:
    void Mirror(TreeNode *pRoot) {
        if(!pRoot)
            return;
        swap(pRoot->left,pRoot->right);
        Mirror(pRoot->left);
        Mirror(pRoot->right);
    }
};

【17】树的子结构

输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

检查tree2是不是tree1的子结构,tree2肯定是比tree1小的树,如果tree1的根与tree2不同,则在tree1的左子树中找与tree2根相同的结点;若在左子树中没有找到,则在右子树中找;如找到,即tree1的某个结点val(极为x)等于tree2根结点的val,则调用子函数判断tree1中以x为根的子树是否与tree2相同,判单的算法是:依次检查root1的左子树与root2的左子树-root1的右子树与root2的右子树。

class Solution {
public:
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
    {
        bool res=false;
        if(pRoot1 && pRoot2)
        {
            if(pRoot1->val == pRoot2->val)
                res=IsSub(pRoot1,pRoot2);
            if(!res)
                res=HasSubtree(pRoot1->left, pRoot2);
            if(!res)
                res=HasSubtree(pRoot1->right, pRoot2);
        }
        return res;
    }
    bool IsSub(TreeNode* root1, TreeNode* root2)
    {
        if(!root2)
            return true;
        if(!root1)
            return false;
        if(root1->val != root2->val)
            return false;
        return IsSub(root1->left, root2->left) && IsSub(root1->right, root2->right);
    }
};

经验:看完思路一定要完全自己写,遇到问题也不能再回去看书上的代码,要自己想明白理清楚,这样才能领会到问题的关键。

【16】合并有序链表

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

难的不是解决问题,而是分析问题。

class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
    {
        ListNode *pMeragedHead=NULL;
        if(!pHead1)
            return pHead2;
        if(!pHead2)
            return pHead1;
        if(pHead1->val < pHead2->val){
            pMeragedHead=pHead1;
            pMeragedHead->next=Merge(pHead1->next,pHead2);
        }else{
            pMeragedHead=pHead2;
            pMeragedHead->next=Merge(pHead1,pHead2->next);
        }
        return pMeragedHead;
    }
};

【15】反转链表

输入一个链表,反转链表后,输出新链表的表头。

由于修改next指针后,会导致链表断开,所以每次需要把后继结点保存下来,代码中的pNext. 当然也需要保存前驱。

class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        ListNode* pReversedHead=NULL;
        ListNode* pNode=pHead;
        ListNode* pre=NULL;
        ListNode* pNext;
        while(pNode)
        {
            pNext=pNode->next;
            if(!pNext){
                pReversedHead=pNode;
            }
            pNode->next=pre;
            pre=pNode;
            pNode=pNext;
        }
        return pReversedHead;
    }
};

遇到的问题:

1. 遍历链表习惯性

pNode=pNode->next;

导致死循环,实际上链表已经断开了。

2. 没有给pReversedHead赋空导致段错误,这说明C++不会自动做这件事。

有两个小技巧:

1. 把pNode初始化为pHead,无须增加链表判空代码。

2. 把pre初始化为空,可以很方便的处理第一个结点:因为第一个结点没有后继,或后继为空。

【14】链表中倒数第k个结点

这个题在某次面试中被问到过…思路是用快慢指针,快指针比慢指针多走k步,这样快指针到达表尾的时候,慢指针指向的结点恰好是倒数第k个。

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
        if(!pListHead) 
            return NULL;
        ListNode *p1,*p2,*pNode;
        p1=pListHead;
        p2=pListHead;
        int i;
        for(i=0;p2;i++)
        {
            p2=p2->next;
            if(i >= k)
                p1=p1->next;
        }
        return (i

 

【12】整数的整数次方

 

问题:给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

一个数的8次方,实际上是两个4次方相乘,利用前面斐波那契数列的思想,避免重复计算:

求解算法与前一致。

class Solution {
public:
    double Power(double base, int exponent) {
        if(exponent == 0)
            return 1;
        if(exponent == 1)
            return base;
        //计算部分积:a^(n/2) * a^(n/2)
        double res = PowerWithUnsignedInt(base, exponent>0 ? exponent >> 1 : (-exponent) >> 1);
        res *= res;
        //如果指数是奇数,还要再乘一个a
        if(exponent & 0x1)//这个判奇数的方法很牛逼了
            res *= base;
        if(exponent < 0)
            res = 1/res;
        return res;
    }
    double PowerWithUnsignedInt(double base, int exponent){
        double tmpRes=1;
        for(int i = 0;i

【11】二进制中1的个数

问题:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

1的二进制最低位为1高位全部为0,与整数相与,若结果为1则表示整数的最低位为1;不断对1左移,就可以判断整数的每一位是否为1. 需要注意的是,更直接的思路虽然是右移这个整数,但当整数是负数时,这样做会出现问题:每次右移之后高位都会补1,这样程序就陷入了死循环。

class Solution {
public:
     int  NumberOf1(int n) {
         int count=0,flag=1;
         while(flag)
         {
             if(n & flag)
                 count++;
             flag = flag << 1;
         }
         return count;
     }
};

【10】矩形覆盖

问题:我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

与跳台阶是一个问题。

class Solution {
public:
    int rectCover(int number) {
        int res[3]={0,1,2};
        if(number<3)
            return res[number];
        int f1=1,f2=2,fn;
        for(int i = 3;i<=number;i++){
            fn=f1+f2;
            f1=f2;
            f2=fn;
        }
        return fn;
    }
};

【9】变态跳台阶

问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

归纳法。

class Solution {
public:
    int jumpFloorII(int number) {
        return pow(2,number-1);
    }
};

 

【8】跳台阶

 

问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

n级台阶的跳法设为f(n). 思考在最后一步,如果跳了1步,则总跳法数为f(n-1);如果最后一步跳了2步,则总跳法数为f(n-2);

即f(n)=f(n-1)+f(n-2). 

是斐波那契数列问题,只不过初始条件有所不同:f(0)=0, f(1)=1, f(2)=2.

求解斐波那契数列的算法与上一题相同。

class Solution {
public:
    int jumpFloor(int number) {
        int res[3]={0,1,2};
        if(number<3)
            return res[number];
        int f1=1,f2=2,fn;
        for(int i = 3;i<=number;i++){
            fn=f1+f2;
            f1=f2;
            f2=fn;
        }
        return fn;
    }
};

【7】斐波那契数列

 

0 1 2 3 4 5
0 1 1 2 3 5

初始条件:f(0)=0, f(1)=1

从2开始,FibN为前两项的和,即One指针+Two指针;

One和Two 的更新:每次求出一个FibN后,One和Two向后移:One指向Two,而Two指向刚算出的和FibN,相当于“遍历”n次就求出了n的f(n),事件复杂度是O(n).

class Solution {
public:
    int Fibonacci(int n) {
        int res[2]={0,1};
        if(n<2)
            return res[n];
        int FibOne=0, FibTwo=1, FibN;
        for(int i = 2;i<=n;i++){
            FibN=FibOne+FibTwo;
            FibOne=FibTwo;
            FibTwo=FibN;
        }
        return FibN;
    }
};

注意res数组的编码习惯~

 

【从行列递增的二维数组中查找关键字】

/*
Matrix是二维数组,每一行、每一列都递增排列
问题:从Matrix中查找某个数target
如果查找成功,返回true,否则返回false
*/

#include
#include

using namespace std;

/*
从右上角数字x开始比较:若大于target,则去除x所在列,因为x下方数字均大于x
					 若小于target,则去除x所在行,因为x前方数字均小于x
二维数组采用一维按行存储的方式
右上角数字的坐标:M[i*cols + j]
*/
bool findInt(vectorM, int rows, int cols, int target)
{
	int i = 0, j = cols - 1;//i j分别扫描行、列

	bool found = false;
	while (M.size()!=0 && i < rows && j >= 0)
	{
		int x = M[i*cols + j];
		if (x > target)
			j--;
		else if (x < target)
			i++;
		else
		{
			found = true;
			break;
		}
	}

	return found;
}

void main()
{
	vector M = { 
		1,2,8,9,
		2,4,9,12,
		4,7,10,13,
		6,8,11,15 };

	bool res = findInt(M, 4, 4, 7);
	cout << "res: " << res;

	system("pause");
}

【替换空格】

问题:实现一个函数,把字符串中的每个空格替换成“%20”.

例如输入“We are happy.”,输出“We%20are%20happy.”

class Solution {
public:
	void replaceSpace(char *str,int length) {
        if(!str || length == 0) {
            return;
        }
        int numOfSpace = 0, originLength = 0;
        for(int i = 0; i < length; i++) {
            originLength++;
            if(str[i] == ' ') {
                numOfSpace++;
            }
        }
        int newLength = originLength + 2 * numOfSpace;
        char *p1 = str + originLength;
        char *p2 = str + newLength;
        while(p1 != p2) {
            if(*p1 != ' ') {
                *p2 = *p1;
                p2--;
            } else {
                *p2-- = '0';
                *p2-- = '2';
                *p2-- = '%';
            }
            p1--;
        }
	}
};

【二叉树中和为某一值的路径】

树的路径:从根节点出发到叶结点所经过的结点形成一条路径。

求出路径上结点值之和为指定值的路径。

 
  1. class Solution {

  2. public:

  3. vector > FindPath(TreeNode* root, int expectNumber) {

  4. vector > res;

  5. if (root == NULL)

  6. return res;

  7.  
  8. //定义栈结构,初始为空

  9. vector path;

  10.  
  11. int currentNumber = 0;

  12.  
  13. FindPath(root, currentNumber, path, expectNumber, res);

  14.  
  15. return res;

  16. }

  17.  
  18. void FindPath(

  19. TreeNode* root,

  20. int currentNumber,

  21. vector path,

  22. const int expectNumber,

  23. vector >& res) {

  24. /*if (root == NULL)

  25. return;*/

  26.  
  27. path.push_back(root->val);

  28. currentNumber += root->val;

  29.  
  30. //如果当前结点为叶结点,且部分和=期待值

  31. bool isLeaf = (root->left == NULL) && (root->right == NULL);

  32. if (currentNumber == expectNumber && isLeaf) {

  33. res.push_back(path);

  34. path.clear();

  35. }

  36. //else {

  37. //如果当前结点不是叶结点,则递归遍历左右子树

  38. if (root->left)

  39. FindPath(root->left, currentNumber, path, expectNumber, res);

  40. if (root->right)

  41. FindPath(root->right, currentNumber, path, expectNumber, res);

  42. //}

  43.  
  44. //返回父结点之前,删除当前结点

  45. path.pop_back();

  46. }

  47. };

【复杂链表的复制】

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空

/*
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
            label(x), next(NULL), random(NULL) {
    }
};
*/
class Solution {
public:
	//算法入口函数
	RandomListNode* Clone(RandomListNode* pHead)
	{
		CloneNodes(pHead);
		ConnectSiblingNodes(pHead);
		return ReconnectNodes(pHead);
	}

	void CloneNodes(RandomListNode* pHead)
	{
		RandomListNode* pNode = pHead;

		while (pNode != NULL)
		{
			//创建副本结点,复制原结点的值,将副本结点链到原结点后
			RandomListNode* pClone = new RandomListNode(0);

			pClone->label = pNode->label;
			pClone->next = pNode->next;
			pClone->random = NULL;//暂时置空,下一个函数再置这一位

			pNode->next = pClone;

			pNode = pClone->next;
		}
	}

	//设置副本结点的random位,指向pNode的random的下一个
	void ConnectSiblingNodes(RandomListNode* pHead)
	{
		RandomListNode* pNode = pHead;
		while (pNode != NULL)
		{
			RandomListNode* pClone = pNode->next;

			if (pNode->random != NULL)
				pClone->random = pNode->random->next;

			pNode = pClone->next;
		}
	}

	RandomListNode* ReconnectNodes(RandomListNode* pHead)
	{
		RandomListNode* pNode = pHead;
		RandomListNode* pCloneHead = NULL;
		RandomListNode* pCloneNode = NULL;
		//设置副本链表头指针!!!!!!!!!!!!!!!!!!
		if (pNode != NULL)
		{
			pCloneHead = pNode->next;
			pCloneNode = pNode->next;

			pNode->next = pCloneNode->next;

			pNode = pCloneNode->next;
		}//now:pNode指向链表中第三个结点,pClone指向链表中第二个结点

		while (pNode != NULL)
		{
			pCloneNode->next = pNode->next;
			pCloneNode = pNode->next;

			pNode->next = pCloneNode->next;

			pNode = pCloneNode->next;
		}
		return pCloneHead;
	}
};

注意感叹号处,由于pNode是一个指针,使用指针时要格外注意,先判断是否为空!如果pNode为NULL,即pNode没有指向任何内存,此时pNode->next会报段错误。

你可能感兴趣的:(剑指offer之编程是一种习惯)