剑指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)。斐波那契数列。。。
代码也就不写了哈。