剑指offer解题思路锦集1-10题

1、【二维有序数组查找

 

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

 

方案肯定是要用O(n)级别的,如果是O(N^2),那么还就是暴力破解。这样根本就没有利用到题目的性质====》二维有序数组,所以还是老老实实想O(N)的算法吧!

 

即存在数组满足以下规律:

	 ------>
	|
	|
	|
	↓
问怎么如何快速找到其中的一个元素?

 

 

存在问题:问题来了,如果从(0,0)出发,那么问题肯定是极度恶心的,因为根本不知道下一次是往下走还是往右边走。。。因为【两个方向都是增加的】

发现问题:这时候我们得知了问题点在于【从(0,0)出发,会使得无论往右还是往下都是增加。】那么是否存在某个点使得【一个方向增加一个方向减少】?
解决问题:使用(0,n-1)点或者(n-1,0)点,会发现一个方向是增加,一个方向是减少。如果值小了,那么就往增加方向查找,如果值大了,就往减少方向减少。这时候代码就十分的轻而易举写出来了。

 

代码:

 

    bool Find(int target, vector<vector<int> > array) {
        int row = array.size();
        int col = array[0].size();
        for (int i = 0,j = col - 1; i < row && j >= 0;)
        {
            if (array[i][j] == target)
            {
                return true;
            }
            if (array[i][j] < target)
            {
                ++i;
            }
            else 
            {
                --j;
            }
        }
        return false;
    }

 

 

 

2、【替换空格】

 

题目:请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

前提:传入的空间是长度足够大的。不然这题就没有啥意义了,简单的计算要申请多少内存,然后申请好了内存,从左到右慢慢拷贝就行了,时间复杂度和空间复杂度都是O(n)。
只有在传入的空间是足够大时候,才可以考虑能不能在O(1)空间复杂度和O(n)时间复杂度时候完成这个事情。

思路:从头开始拷贝也是巨复杂无比的,因为根本不知道拷贝去哪里,会不会覆盖了本身的字符串。。。
问题根源:在于选择了一个错误的方向,这时候我们可以想想能不能从后往前拷贝呢???如果可以,这样思路就变得很简单了。。。

代码:

    int getSpaceNum(char *str){
        int spaceNum = 0;
        if (NULL == str)
        {
            return spaceNum;
        }

        while(*str++ != '\0')
        {
            ++spaceNum;
        }
        return spaceNum;

    }
    void replaceSpace(char *str,int length) {
        int spaceNum = getSpaceNum(str);
        int end = length + spaceNum * 2;
        if end
        while(--length > 0)
        {
            char ch = str[length];
            if (ch == ' ')
            {
                str[--end] = '0';
                str[--end] = '2';
                str[--end] = '%';
            }
            else
            {
                str[--end] = ch;
            }
        }

    }

 

 

3、【从尾到头打印链表】

 

题目:输入一个链表,从尾到头打印链表每个节点的值。

思路:啥也不用想了,,简单的一个尾递归就OK了。

 

问题:从尾到头打印单链表(1,2,3,4,5...n)

分解成:从尾到头打印单链表(2,3,4,5...n) 问题+ 打印值1

简单的分治想法,就完成了。。。

 

代码

    void printListFromTailToHead(ListNode* head, vector<int> &vec)
    {
        if(head == NULL) return;
        printListFromTailToHead(head->next, vec);
        vec.push_back(head->val);
    }
    vector<int> printListFromTailToHead(ListNode* head) {
    	vector<int> vec;
        printListFromTailToHead(head, vec);
        return vec;
    }

 

4、【重建二叉树】

 

题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路:还是使用分治思路啊,老铁。

假设输入为:前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}。

首先得到头结点1,左子树中序遍历序列{4,7,2},右子树中序遍历序列{5,3,8,6}

      再得到左子树前序遍历序列{,2,4,7},右子树中序遍历序列{3,5,6,8}。

接下来不就是分治了么???

 

代码:

  TreeNode* reConstructBinaryTree(vector<int> pre, int preStart, int preEnd, vector<int> vin, int vinStart, int vinEnd) {
        if (preStart > preEnd || vinStart > vinEnd)
        {
            return NULL;
        }
        int value = pre[preStart];
        TreeNode *root = new TreeNode(value);
        for (int i = vinStart; i <= vinEnd; ++i)
        {
            if (value == vin[i])
            {
                int leftTreeSize = i - vinStart;
                root->left = reConstructBinaryTree(pre, preStart+1, preStart + leftTreeSize, vin, vinStart, i - 1);
                root->right = reConstructBinaryTree(pre, preStart + leftTreeSize + 1, preEnd, vin, i + 1, vinEnd);
            }
        }
        return root;
    }

    TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
        int preSize = pre.size();
        int vinSize = vin.size();
        if (preSize == 0 || preSize != vinSize)
        {
            return NULL;
        }
        return reConstructBinaryTree(pre, 0, preSize - 1, vin, 0, vinSize - 1);
    }

 

5、【用两个栈来实现一个队列】

 

题目:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

思路:

  假设存在两个栈stack1、stack2,可以这样认为:

    stack2中保存的为【队列的前部分元素】,可以认为 其栈顶值 = 队列头部值。

    stack1中的元素为【未经转换的队列后部分元素】,相当于buffer一样的存在,它其实就是队列中后部分元素逆序的结果。因此需要将其元素全部投入到stack2中,这样完成翻转过程。

 

画图如下:

 
|  <----      队列        ----> |
|  <- 栈2 ->   |  <- 栈1 ->     |
| 和队列顺序一致 |  和队列顺序相反  |

 

代码如下:

class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }
 
    int pop() {
        if (stack2.size() == 0)
        {
            while ( !stack1.empty())
            {
                stack2.push(stack1.top());
                stack1.pop();
            }
        }
        int value = stack2.top();
        stack2.pop();
        return value;
    }
 
private:
    stack<int> stack1;
    stack<int> stack2;
};

 

6、【旋转数组的最小元素】

题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

 

其实这条题类似二分查找一样,二分查找分析的是中点和左节点,或者分析中点和右节点。而这里也是类似的,要么分析【中点和左节点】,或者【分析中点和右节点】

我们为了简单,就分析中点和左节点。。。

先画图如下:


思考过程:
1、如果发现arr[mid] >= arr[start],那么说明了[start, mid]处于非递减状态,也就是说断点不可能出现在这里。因此这时候可以搜索[mid, end]
2、如果发现arr[mid] < arr[start],那么说明了啥?[start,mid]之间就存在着断点!


有没有什么要注意的点呢?有的!那就是可能存在直线状态,这时候发现了arr[start] == arr[end],这时候只能直线搜索了。。。

 

代码:

    int FidMin(vector<int> &arr,int start,int end)
    {
        int res = arr[start];
        for(int i = start+1;i<=end;++i)
        {
            if(arr[i]>res)res=arr[i];
        }
        return res;
    }
    int minNumberInRotateArray(vector<int> rotateArray) {
        int n = rotateArray.size();
        if(n <= 0) return 0;
       // if(n == 1) return rotateArray[0];
        //下 上
   		int start = 0;
        int end = n-1;
        int mid = (start+end)/2;
        while(rotateArray[start] >= rotateArray[end])
        {
            if(end-start<=1){mid = end;break;}
            mid = (start+end)/2;
            if(rotateArray[start] == rotateArray[end]&&rotateArray[end] == rotateArray[mid])//一条直线
                return FidMin(rotateArray,start,end);
            if(rotateArray[mid] >= rotateArray[start]){start = mid;}
          //  else if(rotateArray[mid] > rotateArray[end]){end = mid;}///////只能else???
            else {end = mid;}
        }
        return rotateArray[mid];
    }

 

7、【斐波那契数列】

题目:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。

这是一条很有意思的题目,有的人看到了就直接脑袋一拍,这不是可以用递归吗?

return n < 2? 1: fib(n-1) + fib(n-2);

不考虑时间复杂度和空间复杂度时候,这是一个解,但是却是一个很糟糕的解,因为其空间复杂度和时间复杂度都很高,接近O(2n) ,准确来说是O(((1+根号5)/2)n)。

 

那么怎么优化呢?最简单的优化思路是,发现到其中存在大量的计算都是重复的,很多计算都是多余的递归,那么我们可不可以搞一个带记忆版本的递归呢?对于已经计算过的斐波那契数列,就不用再算,嗯,大概就是这个意思。。。

根据这个想法不难得到代码如下:

#!/usr/bin/env python
# coding=utf-8

def _fib(n, buff):
    if buff[n] != -1:
        return buff[n]

    buff[n] = _fib(n-1, buff) + _fib(n-2, buff)
    return buff[n]

def fib(n):
    if n <= 0:
        return 0
    if n < 2:
        return 1

    # 初始化
    buff = (n + 1) * [-1]
    buff[0] = 0
    buff[1] = 1
    buff[2] = 1
    return _fib(n, buff)

print fib(1)
print fib(2)
print fib(3)
print fib(4)
print fib(5)
print fib(6)
print fib(7)

 

但是这种解决方案是丑陋的,因为其还是用到了递归,那么我们能不能尝试不用递归呢?我们想到了数学归纳法,也就是所谓的动态规划。它和递归差别在于递归想法是自顶向下,而动态规划是自下往上。

思路如下:

假设我们已知fib(n-1)和fib(n-2)的值,那么下一个值就为fib(n-1)+fib(n-2)。是不是很简单?没错,这东西就好像数学归纳法一样。。。

 

代码如下:

#!/usr/bin/env python
# coding=utf-8

def fib(n):
    if n <= 0:
        return 0
    if n < 2:
        return 1

    # 初始化
    buff = (n + 1) * [-1]
    buff[0] = 0
    buff[1] = 1
    buff[2] = 1
    for i in range(2, n + 1):
        buff[i] = buff[i-1] + buff[i-2]
    return buff[n]

print fib(1)
print fib(2)
print fib(3)
print fib(4)
print fib(5)
print fib(6)
print fib(7)

 

再仔细考虑下代码是否还能继续优化?无疑是存在,因为我们从代码中发现了:每次执行循环都是使用了buff[i-1]和buff[i-2]两个变量。但是其他buff[0:i-1]全部没用。这样我们是不是可以思考:是否不用这个数组,用两个变量来保存?答案是可以的。这就是每一本C语言书上介绍的循环版本的非递归斐波那契计算方法。

def fib(n):
    if n <= 0:
        return 0
    if n < 2:
        return 1

    # 初始化
    f = 1
    f1 = 1
    f2 = 1
    for i in range(2, n):
        f = f1 + f2
        f2 = f1 # 更新下一次的buff[n-2]
        f1 = f  # 更新下一次的buff[n-1]

    return f

print fib(1)
print fib(2)
print fib(3)
print fib(4)
print fib(5)
print fib(6)
print fib(7)

 

还能优化吗???????这里留下了一个深深的疑问?是否有比O(n)更优的算法??必然是存在的。。。就是大名鼎鼎的代入公式就可以了。

高中数学,,,计算特征根方程,,不过这里涉及到数学。。。就不怎么说了,简单贴上一个链接吧:

    求数列通项的特征根法  http://www.360doc.com/content/16/0117/10/2289804_528556906.shtml

我们只取其结果就可以了。

这时候我们直接调数学函数就可以了,不用递归,也不用循环,哈哈。但是是存在问题的,就是如何利用O(lg n)时间复杂度去计算Xn?先看后面章节有没有讲到吧,如果没有的话,那么我再补充一下。。。反正知道这个只要O(lg n)就可以了。。

 

8、【跳台阶】

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

 

这个题是用分治想法来解的,首先青蛙在第一次跳时候,可以跳1步或者2步。

  假设跳1步,那么问题就变成了“青蛙跳n-1个台阶一共有多少种方法”,

  假设跳2步,那么问题就变成了"青蛙跳n-2个台阶一共有多少种方法"。


所以啊,青蛙可以跳一步或两步,也就是说,问题是由 “青蛙跳n-1个台阶一共有多少种方法” 或 “青蛙跳n-2个台阶一共有多少种方法” 两个选项。也就是 fun(n-1)+fun(n-2)。斐波那契数列。。。

 

代码就不给了额,,,

 

9、【变态跳台阶】

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

 

n = 1 return 1
n = 2 return 2
n > 2 return an-1 + an-2 +...+ a2 + a1

也就是说  an = an-1 + an-2 +...+ a2 + a1

又因为 an-1 = an-2 +an-3...+ a2 + a1

所以有an = an-1 +(an-2 +...+ a2 + a1) =  an-1+an-1=2an-1

所以懂了吧,简单一句return 1<< --n就是答案啦~~~

 

代码如下:

    int jumpFloorII(int number) {
		return 1<<--number;
    }

 

10、【矩形覆盖】

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

 

这题基本和青蛙跳台阶一模一样的分析过程。。。

 

这个题也是用分治想法来解的,首先在第一次覆盖时候,可以横着覆盖或者竖着覆盖。

  假设横着覆盖,那么问题就变成了“覆盖2*(n-2)的矩阵一共有多少种方法”,

  假设竖着覆盖,那么问题就变成了"覆盖2*(n-1)的矩阵一共有多少种方法"。


所以啊,横着覆盖或者竖着覆盖,也就是说,问题是由 “覆盖2*(n-2)的矩阵一共有多少种方法” 或 “覆盖2*(n-1)的矩阵一共有多少种方法” 两个选项。也就是 fun(n-1)+fun(n-2)。斐波那契数列。。。

 

代码也就不写了哈。

 

posted @ 2018-05-07 10:57  ExitQuit  阅读(233)  评论(0编辑  收藏  举报