剑指Offer_编程题——记录
1. 二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
简单版:
class Solution { public: bool Find(int target, vector<vector<int> > array) { int Wide_size = array.size(); int Long_size = array[0].size(); int i; int j; for (i = 0;i<Wide_size; i++) { for (j = 0; j < Long_size; j++) { if (target == array[i][j]) { return true; } } } return false; } };
升级版:从数组右上角开始扫描,该值大就往左找,该值小就往下找。
思考:也可以从左下开始,一样的效果;如果从左上和右下走,会在target很大或者很小的时候很快,但是一旦不是这种情况,没有优化效果。
class Solution { public: bool Find(int target, vector<vector<int>> array) { int row = array.size(); int col = array[0].size(); int i = 0; int j = col - 1; while(i < row && j >= 0) { if (array[i][j]==target) { return true; } else if (array[i][j] > target) { j--; //如果这个值比目标值大,就不需要比对这一列了,因为这一列其他值都比这个值大,代表目标值在矩阵的左边 } else { i++; //如果这个值比目标值小,代表目标值在矩阵的下方 } } return false; } };
2. 替换空格
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
思路:
先拿到空格的数目,计算出最终字符串的长度,从终止符开始,往前替换赋值。
本题主要考察对字符串的处理
一般像这种需要向后扩充容量重新整理内存的,最好能够考虑到从尾部开始整理的方法
1.指针都可以当作数组使用,但是指针本身不检查是否超出数组范围。
2.对字符串的处理都应该考虑最后的空字符’\0’。
3.应该一个一个的处理字符串中的字符,不要向一蹴而就。
4.扩充字符串可以考虑从尾部开始。
5.应该警惕内存覆盖,如果改变字符串会导致字符串变长,那应该考虑内存的问题
class Solution { public: void replaceSpace(char *str,int length) { if (str == nullptr || length <= 0)return; int count_of_blank = 0; int original_length = 0; char *tmpStr = str; while(*tmpStr != '\0'){//循环到终止符结束 if(*tmpStr == ' ') count_of_blank++;//获取空格数量 tmpStr++; original_length++;//获取原本字符串长度 } int final_length = original_length + 2*count_of_blank; if(final_length+1>length){//如果length小于替换后的字符串大小 return; } str[final_length--] = '\0'; //设置终止符 for(int i=original_length-1; i>=0; i--){ if(str[i] != ' '){ str[final_length--] = str[i]; } else{ str[final_length--] = '0'; str[final_length--] = '2'; str[final_length--] = '%'; } } } };
3. 从尾到头打印链表。
- 题目:输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
- 思路:使用栈“先进后出”。
/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : * val(x), next(NULL) { * } * }; */ class Solution { public: vector<int> printListFromTailToHead(ListNode* head) { if(head==nullptr) //判断空指针 return vector<int>(); stack<int> tmp_stack; vector<int> res; ListNode *copy_head = head; while(copy_head!=nullptr){//放进栈 tmp_stack.push(copy_head->val); copy_head=copy_head->next; } while(!tmp_stack.empty()){//拿出栈 res.push_back(tmp_stack.top()); tmp_stack.pop();//出栈 } return res; } };
有的公司明确题目要求不能使用额外的节点存储空间,如何在不使用额外存储节点的情况下使一个单链表的所有节点逆序?我们先用迭代/循环的思想来分析这个问题。
https://blog.csdn.net/ljyljyok/article/details/77996029 这个讲的很详细
4.重建二叉树
- 题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
- 思路: 必须注意到前序遍历的第一个节点是根节点,中序遍历的和根节点相同的那个节点(不含重复节点)是根节点且把树分成左右两个子树,左边子树是左子树,右边是右子树,左右两个子树也都是同样的规律,所以可以用递归来做。递归要写的两个要素:终止条件 和 递推公式。
/** * Definition for binary tree * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) { int len = pre.size();//得到vector长度 if(len==0)return nullptr; //递归结束的标志 int root = pre[0];//根节点为前序遍历第一个 TreeNode* ret = new TreeNode(root); //新建一个二叉树节点,并且根节点为root vector<int> left_pre, left_in, right_pre, right_in; int Inroot; for(Inroot=0;Inroot<len;Inroot++){ if(vin[Inroot]==root)//拿到root在中序遍历中的下标值 break; } for(int i=0;i<len;i++){ if(i<Inroot){ left_pre.push_back(pre[i+1]); left_in.push_back(vin[i]); } else if(i>Inroot){ right_pre.push_back(pre[i]); right_in.push_back(vin[i]); } } ret->left=reConstructBinaryTree(left_pre,left_in); ret->right=reConstructBinaryTree(right_pre,right_in); return ret; } };
5. 用两个栈实现一个队列。
- 题目:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
- 思路:用一个栈来入栈,元素入栈的时候都从这里来入栈,如果要删除的话,则利用另外一个空栈来另一个的所有元素取出并压入空栈,然后出栈,每次出栈检查那个栈是否是空的,如果不是空的,直接出栈,空的话则从入栈的栈里出来。
语言描述起来有点麻烦,看图:
class Solution { public: void push(int node) { stack1.push(node); } int pop() { if(stack2.empty()){//如果栈2为空,从栈1取出所有元素压入栈2 while(!stack1.empty()){ stack2.push(stack1.top()); stack1.pop(); } } int res = stack2.top(); stack2.pop(); return res; } private: stack<int> stack1; stack<int> stack2; };
另外,可用两个队列来做一个栈。
思路: 队列的话因为队列都是先进先出,所以如果把一个队列的数字全部复制到另外一个队列的话顺序是没有变的,所以有必要借助两个队列么? 自然是有必要的,虽然顺序没有变,但是我们可以在转移元素的时候把最后一个删除掉,也就是说入栈的时候挑非空的队列入栈,出栈的时候把非空的队列复制到空的中,复制过程中把最后一个元素删掉。
6.旋转数组的最小数字。
- 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
- 思路: 左区数值均大于arr[arr.size()-1],右区数值均小于arr[0]。那么从取中间位置看它是左区还是右区,然后再向最小值位置靠近,就可以找到最小值。这种方法时间效率最高,时间复杂度是O(1)~O(n/2)。
这个是我的写的第一版,AC40%:
class Solution { public: int minNumberInRotateArray(vector<int> rotateArray) { int length = rotateArray.size(); if(length==0){ return 0; } int res = rotateArray[length/2]; if(res<rotateArray[0]){ //右区的值 for(int i=0;i<length/2;i++){ if(rotateArray[length/2-i]<rotateArray[length/2-i-1]) return rotateArray[length/2-i]; } } else if(res>rotateArray[length-1]){//左区的值 for(int i=0;i<length/2;i++){ if(rotateArray[length/2+i]>rotateArray[length/2+i+1]) return rotateArray[length/2+i]; } } else { return res; } } };
参考他人的代码后成功了:
class Solution { public: int minNumberInRotateArray(vector<int> rotateArray) { int length = rotateArray.size(); int res = 0; if(length==0){ return res; } int index = length/2; while(index>0 && index<length){ if (rotateArray[index]<rotateArray[0] ){//在右区 index--; }else if(rotateArray[index]>rotateArray[length-1] && rotateArray[index]<=rotateArray[index+1]){//在左区 index++; }else { res = rotateArray[index + 1]; break; } } return res; } };
考虑的不够全面,左区、右区的范围要事先定好,然后剩下的情况就是找到最小值的情况了。
7. 斐波拉切数列。
- 题目: 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39 。 - 思路: 有递归和循环两种写法,但是递归消耗的资源是很大的,所以还是推荐使用循环的方式来写这个。
class Solution { public: int Fibonacci(int n) { if(n<2) return n; //如果小于2,输出n本身即可 else{ int res = 0; int n0=0,n1=1; for(int i=2;i<=n;i++){ res = (n0 + n1); n0 = n1; n1 = res; } return res; } } };
8.跳台阶问题。
- 问题: 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
- 思路: 虽然是个跳台阶的问题,但是实际上和刚才做的那个斐波那契数列是一样的。分析:向上跳台阶:我们假设跳上N阶台阶的方法有F(N)中,那么很容易可以得到:
F(N)=F(N-1)+F(N-2)
为什么是这样的呢?我们分析其跳最后一次的情况:
如果跳的是两阶,则说明前面有F(N-2)种方式。
加入跳的是一阶,则说明前面有F(N-1)种方式。
所以得到的递推关系就如上。而且我们也很容易可以知道。
F(1)=1,F(2)=2.(1,1或者2)
代码和斐波那契数列那个没什么不同,只要把first改成1基本就可以了。
class Solution { public: int jumpFloor(int number) { if(number<=2) return number; //如果小于2,输出n本身即可 else{ int res = 0; int n1=1,n2=2; for(int i=3;i<=number;i++){ res = (n1 + n2); n1 = n2; n2 = res; } return res; } } };
9.变态跳台阶问题。
- 题目: 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法
- 思路:
//题目虽然变难了,但是实际上程序是更简单了,结合上一个题的分析
/*我们可以得到: F(N)=F(N-1)+F(N-2)+F(N-3)+...+F(1)
把N-1带入则可以得到: F(N-1)=F(N-2)+F(n-3)+...+F(1)
两式相减呢?: F(N)=2*F(N-1) 则这是一个等比数列啊,且F(1)=1,所以最后的结果就是一个2的幂次。
class Solution { public: int jumpFloorII(int number) { if(number<2)return 1; int n1=1; for(int i=2;i<=number;i++) { n1*=2; } return n1; } };
10. 矩形覆盖。
- 题目:我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
- 思路: 从后往前考虑,跟简单跳台阶是一样的。
class Solution { public: int rectCover(int number) { if(number<=3){ return number; } else{ int n2=2,n3=3,res=0; for(int i=4;i<=number;i++){ res=n2+n3; n2=n3; n3=res; } return res; } } };
11.二进制中1的个数。
- 题目: 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
- 思路:n-1的二进制位相比n的二进制位位置相同且值为1的个数少1。
class Solution { public: int NumberOf1(int n) { int res= 0; while(n){ n=n&(n-1); res++; } return res; } };
12.数值的整数次方。
- 题目: 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
- 思路: 主要是考虑指数的各种情况就可以。
class Solution { public: double Power(double base, int exponent) { bool isPositive = true; if(exponent==0){ //任何数的0次方都为1 return 1.0; }else if(exponent<0){ //任何数的负数次方等于正数次方的倒数 exponent = -exponent; isPositive=false; } double res = 1.0; // base的正数exponent次方等于exponent个base相乘。 for(int i=0;i<exponent;i++){ res = res*base; } if(!isPositive){ res = 1.0/res; } return res; } };
13.调整数组顺序使奇数位于偶数前面。
- 题目: 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
- 思路:第一种方法,一个下标找偶数后插到数组,从头到尾找偶数,找到偶数将这个偶数之后的元素依次向前移一位,而把这个偶数放到最后,如果是奇数则跳过。我觉得这个题不难理解,难的是如何判断数组已经符合要求了,也就是循环结束的条件,为了这个循环结束条件,我还定义了一个count变量用来统计已经移动到数组尾部的偶数个数。当下标不小于这个数组长度-count时候结束。还有一个坑是,从头到尾循环的时候,如果找到了偶数,下标不要动。
- 第二种利用STL中的stable_partition和lambda表达式。
class Solution { public: void reOrderArray(vector<int> &array) { int len = array.size(); int count = 0; for(int i = 0;i<len-count;i++){ if(array[i]%2==0){ int tmp = array[i]; for(int j=i ;j<len-1 ;j++){ array[j]=array[j+1]; } array[len-1]=tmp; count++;//记录后半部分的长度 i--; } } } };
14.链表倒数第k个节点。
- 题目:输入一个链表,输出该链表中倒数第k个结点。
- 思路:双指针,end先后移k个,然后first和end同时后移,end到null之后,first指向的就是倒数第k个。细微的+1或者-1自己画一下就知道了。 这是一个单向链表。
- 主要是考虑鲁棒性:k<0或者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(k<0 || pListHead==NULL) return NULL; ListNode* first,* end; first=end=pListHead;
while(k--){ if(end==NULL) return NULL; end=end->next; } while(end){ end=end->next; first=first->next; } return first; } };
15、反转链表
输入一个链表,反转链表后,输出新链表的表头。
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ class Solution { public: ListNode* ReverseList(ListNode* pHead) { if(pHead==NULL||pHead->next==NULL)return pHead; ListNode* pre=pHead; ListNode* cur=pHead->next; ListNode* nex=cur->next; while(cur){ cur->next=pre; pre=cur; cur=nex; if(nex){ nex=nex->next; } } pHead->next=NULL;//注意这句,链表尾的next置为null pHead=pre; return pHead; } };
用栈的方式:
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ class Solution { public: ListNode* ReverseList(ListNode* pHead) { if(pHead==NULL||pHead->next==NULL)return pHead; stack<int> s; ListNode* cur=pHead; while(cur!=nullptr){//压入栈 s.push(cur->val); cur=cur->next; } cur=pHead; while(!s.empty()){ cur->val=s.top(); s.pop();//弹出栈 cur=cur->next; } return pHead; } };
16、合并两个有序的链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路:
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ class Solution { public: ListNode* Merge(ListNode* pHead1, ListNode* pHead2) { if(pHead1==NULL&&pHead2==NULL) return NULL; if(pHead1==NULL) return pHead2; if(pHead2==NULL) return pHead1; ListNode *first; // 两个链表头比较,谁小,谁是新链表头 if(pHead1->val<pHead2->val){ first = pHead1; pHead1 = pHead1->next; }else{ first = pHead2; pHead2 = pHead2->next; } ListNode *list=first; //first保存表头节点,list用来遍历 while(pHead1&&pHead2){ //二者都不为空 if(pHead1->val<pHead2->val){ list->next = pHead1; //pHead1后插进list新链表 pHead1 = pHead1 -> next; //pHead1下标往后移一位 }else{ list->next = pHead2; //pHead2后插进list新链表 pHead2 = pHead2 -> next; } list = list -> next; } if(pHead1==NULL){ list -> next = pHead2; }else if(pHead2==NULL){ list ->next = pHead1; } return first; } };
17、树的子结构
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
/* struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) { } };*/ class Solution { public: bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) { if(!pRoot1) return false; if(!pRoot2) return false; return isSubtree(pRoot1, pRoot2)|| HasSubtree(pRoot1->right, pRoot2)|| HasSubtree(pRoot1->left, pRoot2); } bool isSubtree(TreeNode*p1, TreeNode*p2){ if(!p2) return true; if(!p1) return false; if(p1->val!=p2->val) return false; return isSubtree(p1->left,p2->left) && isSubtree(p1->right,p2->right); } };
18、二叉树的镜像
/* struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) { } };*/ class Solution { public: void Mirror(TreeNode *pRoot) { if(!pRoot) return; TreeNode*tmp=pRoot->left; pRoot->left=pRoot->right; pRoot->right=tmp; Mirror(pRoot->left); Mirror(pRoot->right); } };
19、顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
class Solution { public: vector<int> printMatrix(vector<vector<int> > matrix) { vector<int> res; int row = matrix.size(); int col = matrix[0].size(); //定义好上下左右边 int up=0, down=row-1, left=0, right=col-1; while(up<=down&&left<=right){ for(int i=left;i<=right;i++){ res.push_back(matrix[up][i]); } for(int i=up+1;i<=down;i++){ res.push_back(matrix[i][right]); } if(up!=down){ for(int i=right-1;i>=left;i--){ res.push_back(matrix[down][i]); } } if(right!=left){ for(int i=down-1;i>=up+1;i--){ res.push_back(matrix[i][left]); } } up++; down--; left++; right--; } return res; } };
20、包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
class Solution { public: void push(int value) { data.push(value); if(smin.empty() || value<smin.top()) //smin辅助栈为空或者value比辅助站栈顶小时,当前值就是最小值; smin.push(value); else //辅助栈不为空且value比栈顶大的情况,就不考虑当前value了,再压入一次辅助栈栈顶元素,即当前的最小值 smin.push(smin.top()); } void pop() { if(!data.empty()&&!smin.empty()){ data.pop(); smin.pop(); } } int top() { return data.top(); } int min() { return smin.top(); } private: stack<int> data;//数据栈 stack<int> smin;//辅助栈,用于存放数据栈每次入栈时的最小元素,每个位置的值是数据栈对应值存在时的最小值 };
参考:https://www.cnblogs.com/wanglei5205/p/8622442.html
21、栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
class Solution { public: bool IsPopOrder(vector<int> pushV,vector<int> popV) { int len = pushV.size(); stack<int> data; for(int pushIndex=0, popIndex=0; pushIndex<len; pushIndex++){ data.push(pushV[pushIndex]); while(!data.empty() && data.top()==popV[popIndex]){ data.pop(); popIndex++; } } if(data.empty()) return true; else return false; } };
22. 从上往下打印二叉树
/* struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) { } };*/ class Solution { public: vector<int> PrintFromTopToBottom(TreeNode* root) { vector<int> res; vector<TreeNode*> nodes; if(root==nullptr) return res; nodes.push_back(root); while(!nodes.empty()){ auto tmp = nodes[0]; if(tmp->left)nodes.push_back(tmp->left); if(tmp->right)nodes.push_back(tmp->right); res.push_back(tmp->val); nodes.erase(nodes.begin()); } return res; } };
23. 二叉搜索树的后序遍历序列
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
- 序列最后一个数字是根节点,序列剩余部分分成两部分,前一部分是左子树,后一部分是右子树。
- 拿到根节点,其余部分,遍历中寻找比根节点小的前序列,比根节点大的后序列,遍历时是否满足,所有比根节点小的节点在前,大的在后;
- 用递归实现对左右子树的判断;
- 注意边界:序列为空;没有节点(不是没有根节点)。
class Solution { public: bool VerifySquenceOfBST(vector<int> sequence) { bool result = bts(sequence, 0, sequence.size() - 1); return result; } private: bool bts(vector<int> s, int begin, int end){ // 边界 if(s.empty() || begin > end){ return false; } // 判断是否满足二叉搜索树的后序遍历性质 int i = begin; while(s[i]<s[end]){ i++; } int j=i; for(;j<end;j++){ if(s[j]<s[end]){ return false; } } // 递归对左右子树判断 // 左子树[begin,i-1] bool left = true; if(i>begin){ left = bts(s, begin, i-1); } // 右子树[i,end-1] bool right = true; if(i<end-1){ right = bts(s, i, end-1); } return left && right; } };
50、数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
思路:对于关联式容器map或者set来讲,重复的数值是会插入失败的,那么一旦插入失败,就遇到了第一个重复的数字,使用unordered_set效率很更高。
上述代码时间复杂度是O(n),需要付出的代价是要一个空间是O(n)的哈希表。实际上还有更优的算法,省掉这个哈希表所占的空间。
仔细想想对于这个数组而言,所有元素都在0到n-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) { for (int i = 0; i < length; i++) { if(numbers[i]!=i) { if(numbers[i]==numbers[numbers[i]]) { *duplication=numbers[i]; return true; } swap(numbers[i],numbers[numbers[i]]); } else i++; } return false; } };