最近在看牛客网左程云老师讲解常考面试题(http://www.nowcoder.com/live/courses),感觉讲的非常好,但是给的代码都是java的,故我这里用c++自己写了一遍,作为一个记录,如有错误,请不吝指正。
牛客堂直播视频#常见面试题精讲(二)(2015.6.3)
题目:
2 从5随机到7随机及其扩展
(1)题目
给定一个等概率随机产生1~5的随机函数rand1To5如下:
public int rand1To5() {
return (int) (Math.random() * 5) + 1;
}
除此之外不能使用任何额外的随机机制,请用rand1To5实现等概率随机产生1~7的随机函数rand1To7。
(2)补充题目
给定一个以p概率产生0,以1-p概率产生1的随机函数rand01p如下:
public int rand01p() {
// you can change p as you like
double p = 0.83;
return Math.random() < p ? 0 : 1;
}
除此之外不能使用任何额外的随机机制,请用rand01p实现等概率随机产生1~6的随机函数rand1To6。
分析:思想就是插空+晒选的原理。
(random5()-1)*5产生均匀0,5,10,15,20, 所以(random5()-1)*5+random()-1 会产生0-24间均匀随机数
更通俗一般的如果randomM()产生0~M-1之间的随机数,那么插空一次randomM()*M+randomM()产生0~M^2-1之间的随机数,,
目的要生成N内的随机数,此时如果插空后的最大数仍然小于N,则继续插空,第二次插空产生0~M^3-1之间的随机数
代码:
// 插空+筛选的原理 // 1: 给定了random5()产生1~5这5个整数的等概率事件 现在要生成random7() int random5(){ int x = rand(); if(x > 32000) return random5(); return x%5+1; } // (random5()-1)*5 产生均匀0,5,10,15,20, 所以(random5()-1)*5+random()-1 会产生0-24间均匀随机数 // 更通俗一般的如果randomM()产生0~M-1之间的随机数,那么插空一次randomM()*M+randomM()产生0~M^2-1之间的随机数,, // 目的要生成N内的随机数,此时如果插空后的最大数仍然小于N,则继续插空,第二次插空产生0~M^3-1之间的随机数 int random7(){ int x = 5*(random5()-1)+random5()-1; if(x > 20) return random7(); //晒选的过程 return x%7+1; } // 2: 给定了random5()产生1~5这5个整数的等概率事件 现在要生成random40() int random24(){ // 会产生0~24之间的随机数 return 5*(random5()-1)+random5()-1; } int random40(){ int x = random24()*5 + random5()-1; //会产生0~124之间的随机数 if(x>=120) return random40(); return x%40+1; }
3给定一个无序数组arr,求出需要排序的最短子数组长度。
例如:
arr = [1,5,3,4,2,6,7]
返回4,因为只有[5,3,4,2]需要排序。
分析: 这道题思路在于两次遍历,第一次找到左边是否存在比当前位置大的元素,第二次找到右边是否存在比当前位置元素小的,然后相减,it is amazing
代码:
// 两次遍历,时间复杂度为O(N),第一次找到左边是否存在比当前位置大的元素,第二次找到右边是否存在比当前位置元素小的 // 然后相减,it is amazing int findMinUnsortArray(int *arr, int n){ int beg = n-1, end = 0; int maxElem = arr[0], minElem = arr[n-1]; for(int i = 0; i < n; i++){ if(maxElem <= arr[i]) maxElem = arr[i]; // 找到左边是否存在比当前位置大的元素 else end = i; } for(int i = n-1; i >= 0; i--){ if(minElem < arr[i])beg = i; // 找到右边是否存在比当前位置元素小的。 else minElem = arr[i]; } return beg < end ? end-beg+1 : 0; }
4 最大的leftMax与rightMax之差的绝对值
给定一个长度为N(N>1)的整型数组arr,可以划分成左右两个部分,左部分arr[0..K],右部分arr[K+1..N-1],K可以取值的范围是[0,N-2]。求这么多划分方案中,左部分中的最大值减去右部分最大值的绝对值,最大是多少?
例如[2,7,3,1,1],当左部分为[2,7],右部分为[3,1,1]时,左部分中的最大值减去右部分最大值的绝对值为4。当左部分为[2,7,3],右部分为[1,1]时,左部分中的最大值减去右部分最大值的绝对值为6。还有很多划分方案,但最终返回6。
分析:此题有三种方法,第一种暴力当然时间复杂度为O(N^2),第二种采用预处理的方法,用两个数组,分别记录0~i的最大值和i~n的最大值,需要空间复杂度为O(N)。第三种是最优的方法,不需要额外的空间复杂度,时间复杂度也为O(N). 即最大值减去左半部分或者右半部分尽量小的最大值即arr[0]或者arr[n-1]
代码:时间复杂度为O(N)和空间复杂度O(N)
// 1:首先就是时间复杂度为O(n^2)和O(n)(但空间复杂度也为O(n),预处理) // int findMaxAbsolute2(int *arr, int n){ int *leftMax = new int[n]; int *rightMax = new int[n]; int maxElem = arr[0]; for(int i = 0; i < n; i++){ // 找到[0,i]之间的最大值 maxElem = max(maxElem, arr[i]); leftMax[i] = maxElem; } maxElem = arr[n-1]; for(int i = n-1; i = 0; i--){ // 找到[i, n-1]之间的最大值 maxElem = max(maxElem , arr[i]); rightMax[i] = maxElem; } maxElem = abs(leftMax[0]- rightMax[0]); for(int i = 0; i < n; i++){ maxElem =max(abs(leftMax[i] - rightMax[i]),maxElem); } delete[] leftMax; delete[] rightMax; return maxElem; }
最优方法:
//:2:最优的时间复杂度为O(n) 最大值减去左半部分或者右半部分尽量小的最大值即arr[0]或者arr[n-1] int findMaxAbsolute(int *arr, int n){ int maxElem = arr[0]; for(int i = 1; i < n; i++) maxElem = max(maxElem, arr[i]); // 最大值减去左边部分或者右边部分值最大值尽量小 return (maxElem-arr[0]) > (maxElem-arr[n-1]) ? (maxElem-arr[0]): (maxElem-arr[n-1]); }
5 现在有一种新的二叉树节点类型如下:
publicclass Node {
publicint value;
publicNode left;
publicNode right;
publicNode parent;
publicNode(int data) {
this.value= data;
}
}
该结构比普通二叉树节点结构多了一条指向父节点的parent指针。假设有一棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确的指向自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。在二叉树的中序遍历的序列中,node的下一个节点叫做node的后继节点。
例如,下图的二叉树:
__________6__________
/ \
___3___ ___9___
/ \ / \
1__ 4__ __8 10
\ \ /
2 57
中序遍历的结果为:1,2,3,4,5,6,7,8,9,10
所以节点1的后继为节点2,节点2的后继为节点3,…,节点10的后继为null
分析:这道题就是求后继结点。
代码:
// 找后继结点。 struct Node{ public: int value; Node *left, *right, *parent; Node(int data):value(data){} }; Node *findLeft(Node *p){ while(p->left != NULL) p = p->left; return p; } Node* findPostNode(Node *p){ if(p->right != NULL) return findLeft(p->right); Node* p_parent = p->parent; while(p_parent != NULL){ if(p_parent ->left == p) return p_parent; p = p_parent; p_parent = p->parent; } return p_parent; }
牛客堂直播视频#常见面试题精讲(三)(2015.6.10)
1. 定义局部最小的概念。
arr长度为1时,arr[0]是局部最小。arr的长度为N(N>1)时,如果
arr[0]<arr[1],那么arr[0]是局部最小;如果arr[N-1]<arr[N-2],那么arr[N-1]是局部
最小;如果0<i<N-1,既有arr[i]<arr[i-1]又有arr[i]<arr[i+1],那么arr[i]是局部最小。
给定无序数组arr,已知arr中任意两个相邻的数都不相等,写一个函数,只需返回arr中任
意一个局部最小出现的位置即可。
分析:此题简单方法O(N)搞定,但是是不得分的,因为有O(lgN)时间搞定,很简单。
代码:
O(N)方法:
// O(N)方法 int getLocalValue(int *arr, int n){ int loc = 0; for(int i = 1; i < n; i++){ if(arr[i] < arr[i-1])loc = i; else break; } return loc; }
// O(lgN)方法 int getLocalValue2(int *arr, int n){ if(n == 1 || arr[0] < arr[1]) return arr[0]; if(arr[n-1] < arr[n-2]) return arr[n-1]; int beg = 1, end = n-2; int mid=0; while(beg <= end){ int mid = (beg+end)>>2; if(arr[mid-1] < arr[mid])end = mid-1; else if(arr[mid+1] < arr[mid])beg = mid+1; else return mid; } return beg; }
2.给定一个double类型的数组arr,其中的元素可正可负可0,返回子数组累乘的最大乘积。
例如arr=[-2.5,4,0,3,0.5,8,-1],子数组[3,0.5,8]累乘可以获得最大的乘积12,
所以返回12。
分析:此题设置两个变量用来记录以该结点作为乘积结尾的最大值和最小值,每次都是求取上次最大值和最小值与此次结点乘积的最大值最小值。之前做过:http://blog.csdn.net/lu597203933/article/details/44962163
max[i] =max(arr[i], max[i-1]*arr[i], min[i-1]*arr[i]).
min[i] =min(arr[i], max[i-1]*arr[i], min[i-1]*arr[i]).
代码:
// 求子数组累乘的最大乘积 double getMaxProduct(double *arr, int n){ double minResult = arr[0]; double maxResult = arr[0]; double maxRe=arr[0]; for(int i = 1; i < n; i++){ double temp = max(arr[i], max(maxResult*arr[i], maxResult*arr[i])); minResult = min(arr[i], min(maxResult*arr[i], minResult*arr[i])); maxResult = temp; maxRe = max(maxRe, maxResult); } return maxRe; }
3.给定一棵完全二叉树的头节点head,返回这棵树的节点个数。如果完全二叉树的节点数为N,
请实现时间复杂度低于O(N)的解法。
分析:典型的回溯递归问题,每次都通过分析右子树的高度来判断左子树是否为满二叉树。先求出整个数树的高度h,如果当前层的右子树的高度+当前层数等于h,说明左子树为满,左子树结点数为1<<(h-l)个。如果小于h,表示右子树满,结点个数为1<<(h-l-1)。
函数包含当前层,整个树的高度,结束条件为当前层等于整个树高度,则返回1.
代码:
// 第l+1层树的高度(左子树判断是否为空) int getHeight(Node *p, int l){ while(p != NULL){ l ++; p=p->left; } return l; } // 递归进行求解,每次都要求右子树的高度,所以时间复杂度为h+h-1+h-2+...+1 = O(h^2)=O((lgn)^2) int dfs(Node *p, int l, int h){ // 回溯参数设置 if(l == h) return 1; // 回溯结束条件 int rh = getHeight(p->right, 1); if(rh == h) return (1 << (h-l))+dfs(p->right, l+1, h); else return (1<<(h-l-1)) + dfs(p->left, l+1, h); } // 主调用函数 int getTreeNodeNumber(Node *head){ int h = getHeight(head, 0); return dfs(head, 1, h); }
4.给定两个有序数组arr1和arr2,两个数组长度都为N,求两个数组中所有数的上中位数。
例如:
arr1 ={1,2,3,4};
arr2 ={3,4,5,6};
一共8个数则上中位数是第4个数,所以返回3。
arr1 ={0,1,2};
arr2 ={3,4,5};
一共6个数则上中位数是第3个数,所以返回2。
要求:时间复杂度O(logN)
分析:归并是可以解决的,但时间复杂度为O(N)。因此要二分分析三种情况,很简单,关键注意有多少个元素被压进去了及奇数偶数的情况就行了。
代码:
递归代码:
// 求等长两序列的中位数——递归解法 int findMedianProcess(int *arr1, int start1, int end1, int *arr2, int start2, int end2){ if(end1==start1) return min(arr1[start1], arr2[start2]); int mid1 = (start1 + end1)>>1; int mid2 = (start2+end2)>1; int offset = !((end1-start1+1)%2); // ==0 为奇数 ==1 为偶数 if(arr1[mid1] == arr2[mid2]) return arr1[mid1]; else if(arr1[mid1] >= arr2[mid2])return findMedianProcess(arr1, start1, mid1, arr2, mid2+offset, end2); else return findMedianProcess(arr1, mid1+offset, end1, arr2, start2, mid2); }
迭代代码:
// 迭代 int findMedianProcess2(int *arr1, int start1, int end1, int *arr2, int start2, int end2){ while(start1 <= end1){ if(start1 == end1) return min(arr1[start1], arr2[start1]); int mid1 = (start1+end1)>>1; int mid2 = (start2+end2)>>1; int offset = !((end1-start1+1)%2); // ==0 为奇数 ==1 为偶数 考虑奇数偶数情况 if(arr1[mid1] == arr2[mid2]) return arr1[mid1]; else if(arr1[mid1] >= arr2[mid2]){ end1 = mid1; start2 = mid2+offset; } else{ start1 = mid1+offset; end2 = mid2; } } return 0; }
5.给定两个有序数组arr1和arr2,在给定一个整数k,返回两个数组的所有数中第K小的数。
例如:
arr1 ={1,2,3,4,5};
arr2 ={3,4,5};
K = 1;
因为1为所有数中最小的,所以返回1;
arr1 ={1,2,3};
arr2 ={3,4,5,6};
K = 4;
因为3为所有数中第4小的数,所以返回3;
要求:如果arr1的长度为N,arr2的长度为M,时间复杂度请达到O(log(min{M,N}))。
分析:此题比较难,我们需要对k分三种情况,并调用第4题的答案进行求解。
(1) 如果k小于length1, 就相当于找arr1与arr2前k个元素的上中位数
(2) 如果k在length1和length2 之间,那么就是取arr1与arr2在[k-length1,k-length1+9]之间的上中位数
(3) 第三种情况k大于等于length2 都转换成了求中位数了
代码:
/* 两个有序数组中找到第k大的数,时间复杂度为O(lg{min(M,N)}) 这里的输入假设arr1的长度要小于等于arr2的长度。 */ int getKTopProcess(int *arr1, int start1, int end1, int *arr2, int start2, int end2, int k){ int length1 = end1 - start1 + 1; // 假定length1 <= length2 int length2 = end2 - start2 + 1; if(k <=0 || k >length1+length2) return 0;// 不存在 // 如果k小于length1, 就相当于找arr1与arr2前k个元素的上中位数 if(k <= length1) return findMedianProcess2(arr1, start1, k-1, arr2, start2, k-1); // 如果k在length1和length2 之间,那么就是取arr1与arr2在[k-length1,k-length1+9]之间的上中位数 else if(k < length2){ if(arr2[k-length1-1] >= arr1[end1])return arr2[k-length1-1]; else return findMedianProcess2(arr1, start1, end1, arr2, k-length1, k-length1+9); } // 第三种情况k大于等于length2 都转换成了求中位数了 else{ if(arr2[k-length1-1]>=arr1[end1])return arr2[k-length1-1]; else if(arr1[k-length2-1] >= arr2[end2]) return arr1[k-length2-1]; else return findMedianProcess2(arr1, k-length2, end1, arr2, k-length1, end1); } }
牛客堂直播视频#时间复杂度专题二(2015.06.24)
Manacher算法
【题目】
给定一个字符串str,返回str中的最长回文子串的长度。
【举例】
str=“123”。其中的最长回文子串“1”或者“2”或者“3”,所以返回1。
str=“abc1234321ab”。其中的最长回文子串“1234321”,所以返回7。
分析:manacher算法见:http://blog.csdn.net/lu597203933/article/details/44180939
代码:
// manacher算法,时间复杂度为O(N), 其中包含了4中拓扑结构,只有当i>=mx 或者p[2*id-i]=mx-i时需要扩展,而p[2*id-i]大于或者小于mx-i时不需要进行扩展 // 时间复杂度可以依据变量mx扩展的长度来计算,最多只能扩展到2N的位置(即加入#号后字符串的长度),故时间复杂度为O(N) int manacherAlg(string s){ string str = "#"; for(int i = 0; i < s.size(); i++){ str += s[i]; str += "#"; } int id = 0, mx = 0; int *p = new int[str.size()]; memset(p, 0, sizeof(int)*str.size()); for(int i = 0; i < str.size(); i++){ if(mx > i) p[i] = min(p[2*id-i], mx-i); else p[i]=1; while(i+p[i] < str.size() && i-p[i]>=0 && str[i+p[i]] == str[i-p[i]]) p[i]++; if(i+p[i] > mx){ mx = i + p[i]; id = i; } } int maxLen = 0; for(int i = 0; i< str.size(); i++) maxLen = max(maxLen, p[i]); return maxLen-1; }
【进阶题目】
给定一个字符串str,想通过添加字符的方式使得str整体都变成回文字符串,但要求只能在str的末尾添加字符,请返回在str后面添加的最短字符串。
【举例】
str=“12”。在末尾添加“1”之后,str变为“121”是回文串。在末尾添加“21”之后,str变为“1221”也是回文串。但“1”是所有添加方案中最短的,所以返回“1”。
【要求】
如果str长度为N,解决原问题和进阶问题的时间复杂度都达到O(N)。
分析:使用manacher算法,就一种情况,最后那个元素首次扩展到字符串外面了,则所需要的字符串就是以该除去以该字符为中点的回文串。
string manacherAlg2(string s){ string str = "#"; for(int i = 0; i < s.size(); i++){ str += s[i]; str += "#"; } int id = 0, mx = 0; int *p = new int[str.size()]; memset(p, 0, sizeof(int)*str.size()); for(int i = 0; i < str.size(); i++){ if(mx > i) p[i] = min(p[2*id-i], mx-i); else p[i]=1; while(i+p[i] < str.size() && i-p[i]>=0 && str[i+p[i]] == str[i-p[i]]) p[i]++; if(i+p[i] > mx){ mx = i + p[i]; id = i; } if(mx == str.size()) break; } int size = s.size()-p[id]+1; string result; result.resize(size); for(int i = 0; i < s.size()-p[id]+1; i++){ result[size-i-1] = str[i*2+1]; } return result; }
bfprt算法及其相关
找到无序数组中最小的K个数
【题目】
给定一个无序的整型数组arr,找到其中最小的k个数。
【要求】
如果数组arr的长度为N,排序之后自然可以得到最小的k个数,此时时间复杂度为排序的时间复杂度即O(N*logN)。本题要求读者实现时间复杂度O(N*logK)和O(N)的方法。
分析:bfprt算法见:http://blog.csdn.net/lu597203933/article/details/43418179
代码:
// 线性查找算法(BFPRT), 最坏情况下时间复杂度也为O(N),克服随机选择最坏情况下的困难 /* 1:将n个元素每5个一组,分成n/5(上界)组。 2:取出每一组的中位数,任意排序方法,比如插入排序。 3:递归的调用selection算法查找上一步中所有中位数的中位数,设为x,偶数个中位数的情况下设定为选取中间小的一个。 4: 用x来分割数组,设小于等于x的个数为k,大于x的个数即为n-k。 5. 若i==k,返回x;若i<k,在小于x的元素中递归查找第i小的元素;若i>k,在大于x的元素中递归查找第i-k小的元素。 其时间复杂度为T(n) <= T(n/5)+T(7/10*n)+O(n) */ // 插入排序 void insertionSort(int *arr, int l, int r){ for(int i = l+1; i <= r; i++){ int temp = arr[i]; int j = i-1; while(j >=0 && arr[j]> temp){ arr[j+1]= arr[j]; j--; } arr[j+1] = temp; } } // 找到接近中位数的下标,不是随机选择,选择中位数的中位数的下标 int getPivotId(int *arr, int l, int r){ if(r-l+1 < 5){ insertionSort(arr, l, r); return (r+l)>>1; } int t = l-1; for(int i = l; i+4 <=r; i= i+5){ insertionSort(arr, i, i+4); swap(arr[++t], arr[i+2]); } return getPivotId(arr, l, t); } // 根据BFPRT算法选择的下标值进行划分 int partition2(int *arr, int l, int r){ int pid = getPivotId(arr, l, r); swap(arr[l], arr[pid]); int pivot = arr[l]; int i = l, j = r; while(i < j){ for(; j > i; j--){ if(arr[j] < pivot){ arr[i++]= arr[j]; break; } } for(; i < j; i++){ if(arr[i] > pivot){ arr[j--] = arr[i]; break; } } } arr[i] = pivot; return i; } // 根据BFPRT算法求无序数组中第k小的数据---主调用函数 int BFPRT(int *arr, int l, int r, int k){ int i = partition2(arr, l, r); int p = i - l +1; if(k == p) return arr[i]; else if(k < p) return BFPRT(arr, l, i-1, k); else return BFPRT(arr, i+1, r, k-p); } // 用随机选择算法求取无序数组中最小的k个元素---主调用函数 int BFPRT2(int *arr, int l, int r, int k, vector<int> &result){ int i = partition2(arr, l, r); int p = i - l +1; if(k == p){ for(int j = l; j <= i; j++) result.push_back(arr[j]); return arr[i]; } else if(k < p){ return BFPRT2(arr, l, i-1, k, result); } else{ for(int j = l; j <= i; j++) result.push_back(arr[j]); return BFPRT2(arr, i+1, r, k-p, result); } }
KMP算法
【题目】
给定两个字符串str和match,长度分别为N和M。实现一个算法,如果字符串str中含有字串match,则返回match在str中的开始位置,不含有则返回-1。
【举例】
str=“acbc”,match=“bc”。返回2。
str=“acbc”,match=“bcc”。返回-1。
【要求】
如果match的长度大于str长度(M>N),str必然不会含有match,可直接返回-1。但如果N>=M,要求算法复杂度O(N)。
分析:kmp算法见:http://blog.csdn.net/lu597203933/article/details/41124815
代码:
//next时间复杂度为O(M),可以这样理解:循环中有两个变量,j和k,而j与j-k每次都是增加的或者一个不变,故最多循环2M次,时间复杂度为O(M) void getNextArray(int *next, int n, const string &p){ next[0] = -1; int j = 0, k = -1; // 初始化 while(j < n-1){ if(k == -1 || p[j] == p[k]) next[++j] = ++k; else k = next[k]; } } // kmp时间复杂度可以这样理解,每次都需要将模式串p向前推动,至少一步,而最多推动的长度就是字符串s的长度,故时间复杂度为O(N) int kmpAlg(const string &s, const string &p){ int i = 0, j = 0; int m = s.size(), n = p.size(); int *next = new int[n]; memset(next, 0, sizeof(int)*n); getNextArray(next, n, p); while(i < m && j < n){ if(j == -1 || s[i] == p[j]){ // 这里需要注意啊 i++; j++; }else j = next[j]; } delete []next; if(j == n) return i-j; return -1; }
牛客堂直播视频#数组相关和动态规划(2015.7.8)
第一题:
计算数组的小和
【题目】
数组小和的定义如下:
例如数组s=[1,3,5,2,4,6],在s[0]的左边小于等于s[0]的数的和为0,在s[1]的左边小于等于s[1]的数的和为1,在s[2]的左边小于等于s[2]的数的和为1+3=4,在s[3]的左边小于等于s[3]的数的和为1,在s[4]的左边小于等于s[4]的数的和为1+3+2=6,在s[5]的左边小于等于s[5]的数的和为1+3+5+2+4=15,所以s的小和=0+1+4+1+6+15=27。
给定一个数组s,实现函数返回s的小和。
分析:此题采用归并算法, 与求逆序数一样,只需要改动一处即可。下面分别给出归并排序的代码,然后给出求逆序数的代码,最后给出求数组小和的代码,时间复杂度为O(NlgN)
归并排序:
void mergeSort(int *arr, int l, int r, int*arr1){ if(r == l) return; int mid = (l + r)>>1; mergeSort(arr, l, mid, arr1); mergeSort(arr, mid+1, r, arr1); int i = l, j = mid+1; int k = l; while(i <= mid && j <= r){ if(arr[i] < arr[j]){ arr1[k++] = arr[i++]; }else{ arr1[k++] = arr[j++]; } } if(i == mid+1){ while(j <=r){ arr1[k++] = arr[j++]; } } if(j == r+1){ while(i <= mid){ arr1[k++] = arr[i++]; } } for(int i = l; i <= r; i++) arr[i] = arr1[i]; }
逆序数:
// 归并算法求解逆序数 void inverseNumbers(int *arr, int l, int r, int*arr1, int &nums){ if(r == l) return; int mid = (l + r)>>1; inverseNumbers(arr, l, mid, arr1, nums); inverseNumbers(arr, mid+1, r, arr1, nums); int i = l, j = mid+1; int k = l; while(i <= mid && j <= r){ if(arr[i] <= arr[j]){ arr1[k++] = arr[i++]; }else{ arr1[k++] = arr[j++]; nums += mid-i+1; // 不同点 用归并求逆序数 } } if(i == mid+1){ while(j <=r){ arr1[k++] = arr[j++]; } } if(j == r+1){ while(i <= mid){ arr1[k++] = arr[i++]; } } for(int i = l; i <= r; i++) arr[i] = arr1[i]; }
数组小和:
void getMinSum(int *arr, int l, int r, int*arr1, int &nums){ // 结果存放在nums中 if(r == l) return; int mid = (l + r)>>1; getMinSum(arr, l, mid, arr1, nums); getMinSum(arr, mid+1, r, arr1, nums); int i = l, j = mid+1; int k = l; while(i <= mid && j <= r){ if(arr[i] <= arr[j]){ nums += arr[i]*(r-j+1); // 归并算法求小和 不同点 arr1[k++] = arr[i++]; }else{ arr1[k++] = arr[j++]; } } if(i == mid+1){ while(j <=r){ arr1[k++] = arr[j++]; } } if(j == r+1){ while(i <= mid){ arr1[k++] = arr[i++]; } } for(int i = l; i <= r; i++) arr[i] = arr1[i]; }
第二题:
数组中未出现的最小正整数
【题目】
给定一个无序整型数组arr,找到数组中未出现的最小正整数。
【举例】
arr=[-1,2,3,4]。返回1。
arr=[1,2,3,4]。返回5。
分析:此题之前做过了。代码如下:
// 求数组中未出现的最小正整数 int findMinInteger(int* arr, int n){ for(int i = 0; i < n; i++){ while(arr[i]>0 && arr[i]<=n && arr[arr[i]-1] != arr[i]){ arr[arr[i]-1] = arr[i]; } } for(int i = 0; i < n; i++) if(arr[i] != i+1) return i+1; return n+1; } // 牛客网给的代码 int findMinInteger2(int* arr, int n){ int l = 0, r = n; while(l < r){ if(arr[l] = l+1){ l++; }else if(arr[l] <= l || arr[l] >r || arr[arr[l]-1]==arr[l]){ arr[l] = arr[--r]; }else{ swap(arr[l], arr[arr[l]-1]); } } return l+1; }
第三题:
数组排序之后相邻数的最大差值
【题目】
给定一个整型数组arr,返回如果排序之后,相邻两数的最大差值。
【举例】
arr=[9,3,1,10]。如果排序,结果为[1,3,9,10],9和3的差为最大差值,故返回6。
arr=[5,5,5,5]。返回0。
【要求】
如果arr的长度为N,请做到时间复杂度为O(N)。
分析:正常思路先排序后得出结果,时间复杂度为O(NlgN),空间复杂度为O(1)
// 将n个元素放入n+1个桶中,根据鸽巢原理必然能够得出有一个桶是空的,这样最大邻近差值一定是在桶间,此时这两个桶中有空桶。
// 最小元素放在1号桶中 最大元素放在最后一个n+1号桶中。——以空间换时间
// 排序后的相邻的最大差值 // 正常思路先排序后得出结果,时间复杂度为O(NlgN),空间复杂度为O(1) // 将n个元素放入n+1个桶中,根据鸽巢原理必然能够得出有一个桶是空的,这样最大邻近差值一定是在桶间,此时这两个桶中有空桶。 // 最小元素放在1号桶中 最大元素放在最后一个n+1号桶中。——以空间换时间 int bucket(int num, int len, int maxVal, int minVal){ // 得到该元素的桶号 return (int)((num-minVal)*len/(maxVal-minVal+0.0)); } int findMinusValue(int *arr, int n){ int minVal = arr[0], maxVal = arr[0]; for(int i = 0; i < n; i++){ minVal = min(minVal, arr[i]); maxVal = max(maxVal , arr[i]); } if(minVal == maxVal) return 0; bool *flag = new bool[n+1]; // false 表示空桶 int *min_arr = new int[n+1]; // 用来存放桶中的最小值 int *max_arr = new int[n+1]; // 用来存放桶中的最大值 memset(flag, false, sizeof(bool)*(n+1)); //memset(min_arr, 0, sizeof(int)*(n+1)); //memset(max_arr, 0, sizeof(int)*(n+1)); for(int i = 0; i< n; i++){ int bid = bucket(arr[i], n, maxVal, minVal); min_arr[bid] = flag[bid] ? min(min_arr[bid], arr[i]):arr[i]; max_arr[bid] = flag[bid] ? max(max_arr[bid], arr[i]):arr[i]; flag[bid] = true; } int max_result = max_arr[0], result=0; for(int i = 1; i < n+1; i++){ if(flag[i]){ result = max(min_arr[i]-max_result, result); max_result = max_arr[i]; } } delete []flag; delete []min_arr; delete []max_arr; return result; }
第四题:
动态规划的空间优化方法
分析:动态规划用一个数组就可以了,没必要用二维数组,之前写的代码都是用二行的二维数组,这里采用一一维数组就可以搞定了。
代码:
// 动态规划问题求方案数 空间复杂度可以压缩在一个一维数组n维 假定n小于m解决 //如果i!=0 dp[j] = min{dp[j-1], dp[j]}+arr[i][j] else dp[0]=dp[0]+arr[i][0] int dpAlg(int **arr, int m, int n){ int* dp = new int[n]; dp[0]= arr[0][0]; for(int i = 1; i < n; i++) dp[i] = dp[i-1] + arr[0][i]; for(int i = 1; i < m; i++){ dp[0] = dp[0]+arr[i][0]; for(int j = 1; j < n; j++) dp[j] = min(dp[j], dp[j-1])+arr[i][j]; } return dp[n-1]; }
牛客堂直播视频#常见面试题精讲(五)(9.9)
题目一
给定一个数组arr,该数组无序,但每个值均为正数,再给定一个正数k。求arr的所有子数组中所有元素相加和为k的最长子数组长度。例如,arr=[1,2,1,1,1],k=3。累加和为3的最长子数组为[1,1,1],所以结果返回3。
要求:时间复杂度O(N),额外空间复杂度O(1)。
分析:这道题也做过了,滑动窗口+双指针的思想。
代码:
// 由于都是整数,采用双指针+滑动窗口 int cattle15_1(int *arr, int n, int k){ int first = 0, last = 0; int sum = 0; int max_length = 0; while(last < n){ if(sum == k){ max_length = max(max_length, last-first+1); sum = sum - arr[first++]+arr[++last]; }else if(sum < k){ last++; sum = sum + arr[last]; } else { first++; sum = sum - arr[first]; } } return max_length; }
题目二
给定一个无序数组arr,其中元素可正、可负、可0,给定一个整数 k。求arr所有的子数组中累加和小于或等于k的最长子数组长度。例如:arr=[3,-2,-4,0,6],k=-2,相加和小于或等于-2的最长子数组为{3,-2,-4,0},所以结果返回4。
分析://如果用暴力的话,直接O(N^2)就搞定了,也就是找所有的连续子数组
//这里进行预处理,以该结点结束的所有元素和,在该结点之前找到第一个和大于sum-k的下标,//以空间换时间,得到的时间复杂度为O(nlgn)。
代码:
// 如果用暴力的话,直接O(N^2)就搞定了,也就是找所有的连续子数组 //这里进行预处理,以该结点结束的所有元素和,在该结点之前找到第一个和大于sum-k的下标 //以空间换时间,得到的时间复杂度为O(nlgn)。 // 主函数 int find_binary_val(int *arr, int l, int r, int k){ while(l <= r){ int mid = (l+r)>>1; if(arr[mid] == k){ if(mid == l || arr[mid] > arr[mid-1] ) // 二分不同点 return mid; else r = mid - 1; } else if(arr[mid] < k) l = mid+1; else r = mid-1; } return l; } int cattle15_2(int *arr, int n , int k){ int *sum = new int[n]; // 用于保存前i项和 sum[0] = arr[0]; for(int i = 1; i < n; i++){ sum[i] = sum[i-1]+arr[i]; } for(int i = 1; i< n; i++){ if(sum[i] < sum[i-1]) sum[i] = sum[i-1]; } int max_length = 0; int sum_result = 0; for(int i = 0; i < n; i++){ sum_result += arr[i]; int j = find_binary_val(sum, 0, i, sum_result-k); //找到第一个大于等于sum_result-k 的元素所在 if(sum[j] == sum_result - k) j = j+1; // 案例相等的情况 就不包含该点 max_length = max(max_length, i-j+1); } delete []sum; return max_length; }
题目三
给定一个N*N的矩阵matrix,在这个矩阵中只有0和1两种值,返回边框全是1的最大正方形的边长长度。
例如:
01111
01001
01001
01111
01011
其中,边框全是1的最大正方形的大小为4*4,所以返回4。
分析:暴力解法时间复杂度为O(N^4),针对每一个点,我们都要看该点所对应的最长边长,直到最小边长都等于1,
// 优化:预处理空间复杂度为O(N^2),A[i][j]表示该点下方有多少个连续的1,B[i][j]表示该点右边有多少个连续的1,
//每次取A[i][j]与B[i][j]的最小值构成的正方形是否都为1,然后逐渐减1,时间复杂度为O(N^3)
代码:
// 暴力解法时间复杂度为O(N^4),针对每一个点,我们都要看该点所对应的最长边长,直到最小边长都等于1, // 优化:预处理空间复杂度为O(N^2),A[i][j]表示该点下方有多少个连续的1,B[i][j]表示该点右边有多少个连续的1, //每次取A[i][j]与B[i][j]的最小值构成的正方形是否都为1,然后逐渐减1,时间复杂度为O(N^3) int getLongestLength(int **arr, int m, int n){ int **A = new int*[m]; int **B = new int*[m]; for(int i = 0; i < m ; i++){ A[i] = new int[n]; B[i] = new int[n]; } for(int j = 0; j < n; j++) // A的最下层进行赋值 A[m-1][j] = arr[m-1][j]; for(int i = m-2; i >=0; i--){ //A的最右层进行赋值 A[i][n-1] = 0; if(arr[i][n-1] == 1) A[i][n-1] = A[i+1][n-1]+1; } for(int i = 0; i < m; i++) // B的最右层进行赋值 B[i][n-1] = arr[i][n-1]; for(int j = n-2; j >= 0; j--){ //B的最下层进行赋值 B[m-1][j] = 0; if(arr[m-1][j] == 1) B[m-1][j] = B[m-1][j+1]+1; } for(int i = m-2; i >= 0; i--){ for(int j = n-2; j >= 0; j--){ if(arr[i][j] == 0){ A[i][j] = 0; B[i][j] = 0; } else{ A[i][j] = A[i+1][j] + 1; B[i][j] = B[i][j+1] + 1; } } } int max_length = 0; for(int i =0; i < m; i++){ for(int j = 0; j < n;j++){ if(arr[i][j] == 0) continue; int l = min(A[i][j], B[i][j]); while(l>=1){ if(B[i+l-1][j] >= l && A[i][j+l-1] >= l) max_length = max(l, max_length); l--; } } } for(int i = 0; i < m; i++){ delete []A[i]; delete []B[i]; } delete []A; delete []B; return max_length; }
题目四
给定一个整型矩阵matrix,其中的值只有0和1两种,求其中全是1的所有矩形区域中,最大的矩形区域为1的数量。
例如:
1110
其中,最大的矩形区域有 3个 1,所以返回3。
再如:
1011
1111
1110
其中,最大的矩形区域有6个1,所以返回6。
分析: // 思路,采用一个预处理数组O(M),时间复杂度为O(M*N),对于每一行,我们计算以该行为底的直方图的面积,然后找到全局最大面积
// 优化算法O(N) 大于就压栈,压入的是数组元素对应的下标,小于等于就弹出栈结算。 空间复杂度为O(N)-----求最大面积 这个比较关键O(N)
代码:
// 思路,采用一个预处理数组O(M),时间复杂度为O(M*N),对于每一行,我们计算以该行为底的直方图的面积,然后找到全局最大面积 // 优化算法O(N) 大于就压栈,压入的是数组元素对应的下标,小于等于就弹出栈结算。 空间复杂度为O(N) int max_contained_rect(int *hist, int n){ if(n == 1) return hist[0]; if(n == 0) return 0; stack<int> s; s.push(-1); // 可以将边界一起考虑进来 s.push(0); int max_area = 0; int i= 1; while(i < n || s.top() != -1){ // s.top()!=-1主要用来处理剩余元素在栈中的情况 int top = s.top(); if(i < n && (top == -1 || hist[i] > hist[top])){ s.push(i); i++; }else{ s.pop(); max_area = max(max_area,(i-s.top()-1)*hist[top]); // 面积为当前i减去栈顶值-1乘以高度 } } return max_area; } // 主函数 int getLargestArea(int **arr, int m, int n){ int *hist = new int[n]; memset(hist, 0, sizeof(int)*n); int max_area = 0; for(int i = 0; i < m; i++){ for(int j = 0; j < n; j++){ // 求直方图数组 if(arr[i][j] == 0)hist[j] = 0; else hist[j] += arr[i][j]; } int area = max_contained_rect(hist, n); max_area = max(area, max_area); } return max_area; }
#牛客堂直播视频#斐波那契数列(2015.7.22)
1、斐波那契系列问题的递归和动态规划
【题目】
给定整数N,返回斐波那契数列的第N项。
分析:形似f(n) = a*f(n-1)+b*f(n-2)形式的式子都可以在O(lgN)时间内解决,而实际就是求一个矩阵的n次方的问题。而如求10^75次方,实际只是求出10^1, 10^2,10^4,10^8,10^16,10^32,10^64,而10^75即为75的二进制1001011所在为1所对应的元素相乘10^1*10^2*10^8*10^64.
(1)求一个数的n次方或者求取一个矩阵的n次方的代码如下:
// 求一个数m的n次方 时间复杂度O(lgN) 实质循环的次数是N这个二进制所对应的位数 也就是O(lgN) int numPower(int m, int n){ int p = n; int res = 1; int tmp = m; for(; p != 0; p >>= 1){ if((p & 1) != 0){ res = res *tmp; } tmp = tmp * tmp; } return res; } // 求一个矩阵A的k次方,时间复杂度为O(lgN) // 两个矩阵相乘 void matrixMulti(int **A, int **B, int m, int **res){ for(int i = 0; i< m; i++){ for(int j = 0; j < m; j++){ res[i][j] = 0; for(int k = 0; k < m; k++){ res[i][j]+=A[i][k]*B[k][j]; } } } } // 矩阵相乘 void copyMatrix(int **A, int **B, int m){ for(int i = 0; i < m; i++){ for(int j = 0; j < m; j++) A[i][j] = B[i][j]; } } // 主函数, result是单位矩阵 m是A的阶数,而k是次方数 void matrixPower(int **A, int m, int k, int **result){ int **tmp = new int*[m]; int **res = new int*[m]; for(int i = 0; i < m; i++){ tmp[i] = new int[m]; res[i] = new int[m]; memset(tmp[i], 0, sizeof(int)*m); memset(res[i], 0, sizeof(int)*m); } for(int i = 0; i < m; i++){ for(int j = 0; j < m; j++){ tmp[i][j] = A[i][j]; } } for(; k != 0; k >>= 1){ if((k & 1) != 0){ matrixMulti(result, tmp, m, res); copyMatrix(result, res, m); } matrixMulti(tmp, tmp, m, res); copyMatrix(tmp, res, m); } for(int i = 0; i < m; i++){ delete []tmp[i]; delete []res[i]; } delete []tmp; delete []res; }
(2)斐波拉协递归方法和lg(N)时间解法的代码:
// 递归方法 int f2(int n){ if(n < 1) return 0; if(n == 1 || n== 2) return 1; int t1 = 1, t2 = 1; int result = 0; for(int i = 3; i <= n; i++){ result = t1+t2; t1 = t2; t2 = result; } return result; } // lgN方法 int f3(int n){ if(n < 1) return 0; if(n == 1 || n == 2)return 1; int a[2][2] = {{1,1},{1,0}}; int **A = new int*[2]; int **result = new int*[2]; for(int i = 0; i< 2; i++){ A[i] = new int[2]; result[i] = new int[2]; memset(result[i], 0, sizeof(int)*2); } for(int i = 0; i < 2; i++){ for(int j = 0; j < 2; j++){ A[i][j] = a[i][j]; if(i == j)result[i][i] = 1; } } matrixPower(A, 2, n-2, result); int res = result[0][0]+result[1][0]; for(int i = 0; i < 2; i++){ delete []result[i]; delete []A[i]; } delete []result; delete []A; return res; }
2、换钱的方法数
【题目】
给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。
【举例】
arr=[5,10,25,1],aim=0。
组成0元的方法有1种,就是所有面值的货币都不用。所以返回1。
arr=[5,10,25,1],aim=15。
组成15元的方法有6种,分别为3张5元,1张10元+1张5元,1张10元+5张1元,10张1元+1张5元,2张5元+5张1元,15张1元。所以返回6。
arr=[3,5],aim=2。
任何方法都无法组成2元。所以返回0。
分析:这道题可以先用暴力递归进行求解,即而转用记忆化搜索方法进行求解,最后可以用dp,这里给出合并后的dp算法。
暴力递归:
// 暴力递归+剪枝 void dfs(int depth, int n, int *arr, int aim, int &count){ if(aim < 0) return; // 剪枝 if(depth == n){ if(aim == 0)count ++; return ; } for(int i = 0; i <= aim/arr[depth]; i++){ dfs(depth+1, n, arr, aim-i*arr[depth], count); } } int exchangeMoney(int *arr, int n, int aim){ int count = 0; dfs(0, n, arr, aim, count); return count; }
记忆化搜索:
// 记忆化搜索方法 int dfsMemory(int depth, int n, int *arr, int aim, int **memory){ if(aim < 0) return 0; int res = 0; if(depth == n){ return res = (aim == 0 ? 1:0); } for(int i = 0; i <= aim/arr[depth]; i++){ int memoryValue = memory[depth+1][aim - arr[depth]*i]; if(memoryValue == 0) // == -1 表示为0, ==0表示未访问 res +=dfsMemory(depth+1, n, arr, aim-arr[depth]*i, memory); else res = (memoryValue == -1 ? 0:memoryValue); } memory[depth][aim] = (res == 0 ? -1 : res); return res; } int exchangeMoney3(int *arr, int n, int aim){ int **memory = new int*[n+1]; for(int i = 0; i <= n; i++){ memory[i] = new int[aim+1]; memset(memory[i], 0, sizeof(int)*(aim+1)); } int res = dfsMemory(0, n, arr, aim, memory); for(int i = 0; i <= n; i++) delete []memory[i]; delete []memory; return res; }
合并dp:
// 动态规划 合并后的动态规划,包括状态压缩之后的了。 int exchangeMoney2(int *arr, int n, int aim){ int *dp = new int[aim+1]; memset(dp, 0, sizeof(int)*(aim+1)); for(int i = 0; i <= aim; i++){ if(i%arr[0] == 0) dp[i] = 1; } for(int i = 1; i < n; i++){ for(int j = 0; j <= aim; j++){ if(j >= arr[i]){ dp[j] = dp[j] + dp[j-arr[i]]; } } } int result = dp[aim]; delete []dp; return result; }
3:二叉树的先序遍历,中序和后序遍历,非递归的先序中序后序遍历及层次遍历
代码:
// 先序遍历 void BTree::preOrder(BNode *p){ if(p != NULL){ cout << p->value<< " "; preOrder(p->l); preOrder(p->r); } } // 中序遍历 void BTree::inOrder(BNode *p){ if(p != NULL){ inOrder(p->l); cout << p->value<< " "; inOrder(p->r); } } // 递归后序遍历 void BTree::postOrder(BNode *p){ if(p != NULL){ postOrder(p->l); postOrder(p->r); cout << p->value<< " "; } } // 非递归前序遍历 void BTree::FpreOrder(BNode *p){ stack<BNode*> s; while(p != NULL || !s.empty()){ while(p != NULL){ cout << p->value << " "; s.push(p); p = p->l; } if(!s.empty()){ BNode *q = s.top(); s.pop(); p = q->r; // 非递归前序遍历 } } } // 非递归中序遍历 void BTree::FinOrder(BNode *p){ stack<BNode*> s; while(p != NULL || !s.empty()){ while(p != NULL){ s.push(p); p = p->l; } if(!s.empty()){ BNode *q = s.top(); s.pop(); // 注意这里有pop cout << q->value << " "; p = q->r; } } } // 后序非递归遍历 void BTree::FpostOrder(BNode *p){ stack<BNode*> s; vector<bool> flag; while(p != NULL || !s.empty()){ while(p != NULL){ s.push(p); flag.push_back(0); p = p->l; } while(!s.empty() && flag[flag.size()-1] == 1){ cout << s.top()->value << " "; s.pop(); flag.pop_back(); } if(!s.empty()){ BNode *q = s.top(); flag[flag.size()-1] = 1; // 注意这里没有pop p = q->r; } } } // 层次遍历 void BTree::levelOrder(BNode *p){ if(p == NULL) return; queue<BNode*> q; q.push(p); while(!q.empty()){ BNode *tmp = q.front(); cout << tmp->value << " "; q.pop(); if(tmp->l != NULL)q.push(tmp->l); if(tmp->r != NULL)q.push(tmp->r); } }
#牛客堂直播视频#经典动态规划题目大串讲(2015.8.5)
1、最小编辑代价
【题目】
给定两个字符串str1和str2,再给定三个整数ic、dc和rc分别代表插入、删除和替换一个字符的代价,返回将str1编辑成str2的最小代价。
【举例】
str1=“abc”,str2=“adc”,ic=5,dc=3,rc=2。
从“abc”编辑成“adc”,把’b’替换成’d’是代价最小的。所以返回2。
str1=“abc”,str2=“adc”,ic=5,dc=3,rc=100。
从“abc”编辑成“abd”,先删除’b’然后插入’d’是代价最小的。所以返回8。
str1=“abc”,str2=“abc”,ic=5,dc=3,rc=2。
不用编辑了,本来就是一样的字符串。所以返回0。
分析:
// 此题为求最小编辑距离,采用动态规划的思想,dp[i][j]用来表示str1[0,1,2,3...i-1]变成str2[0,1,2,3...j-1]最小代价是多少
/*有四种情况,当str1[i-1]==str2[j-1]时, dp[i][j] = dp[i-1][j-1];否则取三种情况的最小值
dp[i][j] = dp[i-1][j]+dc; // 其中dc为删除代价
dp[i][j] = dp[i][j-1]+ic; // ic为插入代价
dp[i][j] = dp[i-1][j-1]+rc; // rc为替换代价
*/
代码:
// 此题为求最小编辑距离,采用动态规划的思想,dp[i][j]用来表示str1[0,1,2,3...i-1]变成str2[0,1,2,3...j-1]最小代价是多少 /*有四种情况,当str1[i-1]==str2[j-1]时, dp[i][j] = dp[i-1][j-1]; 否则取三种情况的最小值 dp[i][j] = dp[i-1][j]+dc; // 其中dc为删除代价 dp[i][j] = dp[i][j-1]+ic; // ic为插入代价 dp[i][j] = dp[i-1][j-1]+rc; // rc为替换代价 */ string str1, str2; int ic, dc, rc; int dp[256][256]; void calc_min_dist(){ while(cin >> str1 >> str2){ cin >> ic >> dc >> rc; // 插入删除替换代价 memset(dp, 0, sizeof(int)*256*256); int m = str1.size(); int n = str2.size(); for(int i = 1; i <= m; i++) dp[i][0] = dp[i-1][0]+ic; for(int j = 1; j <= n; j++) dp[0][j] = dp[0][j-1] + ic; for(int i = 1; i <=m; i++){ for(int j = 1; j <= n; j++){ if(str1[i-1] == str2[j-1]) dp[i][j] = dp[i-1][j-1]; else dp[i][j] = min(dp[i-1][j-1]+rc, min(dp[i][j-1]+ic, dp[i-1][j]+dc)); } } cout<< dp[m][n] << endl; } }
优化版本,状态压缩,使用了一个tmp变量来表示dp[i-1][j-1]
// 优化版本 压缩动态规划,空间复杂度降低到O({M,N}) /* 其中用了一个tmp2表示上一次的dp[i-1][j-1],, 而dp[i-1]表示0~i到0~j-1变换。dp[i]表示0~i-1到0~j变换。。 画一个二维数组就知道了。 */ int dp2[256]; void calc_min_dist2(){ while(cin >> str1 >> str2){ cin >> ic >> dc >> rc; // 插入删除替换代价 memset(dp2, 0, sizeof(int)*256); int m = str1.size(); int n = str2.size(); for(int j = 1; j <= n; j++) dp2[j] = dp2[j-1] + ic; int tmp1=0, tmp2=0; for(int i = 1; i <= m; i++){ for(int j = 0; j <= n; j++){ int tmp1 = dp2[j]; if(j == 0) dp2[j] = dp2[j]+ic; else dp2[j] = min(tmp2+rc, min(dp2[j-1]+ic, dp2[j]+dc)); tmp2 = tmp1; } } cout<< dp2[n] << endl; } }
2、字符串的交错组成
【题目】
给定三个字符串str1、str2和aim。如果aim包含且仅包含来自str1和str2的所有字符,而且在aim中属于str1的字符之间保持原来在str1中的顺序,属于str2的字符之间保持原来在str2中的顺序,那么称aim是str1和str2的交错组成。实现一个函数,判断aim是否是str1和str2交错组成。
【举例】
str1=“AB”,str2=“12”。那么“AB12”、“A1B2”、“A12B”、“1A2B”和“1AB2”等等都是str1和str2交错组成。
分析: dp[i][j]用来表示str1从0~i-1,str2从0~j-1能否交错表示aim[0~i+j-1]. 如果str1[i-1]==aim[i+j-1] 表示aim[i+j-1] 来自str1[i-1]. 如果str2[j-1]==aim[i+j-1] 表示aim[i+j-1] 来自str2[j-1]
代码:
/* dp[i][j]用来表示str1从0~i-1,str2从0~j-1能否交错表示aim[0~i+j-1] */ void cattle10_1(){ string str1, str2, aim; while(cin >> str1 >> str2 >> aim){ int m = str1.size(); int n = str2.size(); bool **dp = new bool*[m+1]; for(int i = 0; i <= m; i++){ dp[i] = new bool[n+1]; memset(dp[i], 0, sizeof(bool)*(n+1)); } dp[0][0] = true; // 初始化 for(int j = 1; j <= n; j++){ if(str2[j-1] == aim[j-1])dp[0][j] = true; else break; } for(int i = 1; i <= m; i++){ if(str1[i-1] == aim[i-1])dp[i][0] = true; else break; } for(int i = 1; i <= m; i++){ for(int j = 1; j <= n; j++){ if(str1[i-1] == aim[i+j-1] && dp[i-1][j]) dp[i][j] = true ; // aim[i+j-1]来自str1[i-1] if(str2[j-1] == aim[i+j-1] && dp[i][j-1]) dp[i][j] = true; // aim[i+j-1]来自str2[j-1] } } cout << dp[m][n] << endl; for(int i = 0; i <= m; i++){ delete []dp[i]; } delete []dp; } }
3、表达式得到期望结果的组成种数
【题目】
给定一个只由0(假)、1(真)、&(逻辑与)、|(逻辑或)和^(异或)五种字符组成的字符串express,再给定一个布尔值desired。返回express能有多少种组合方式,可以达到desired的结果。
【举例】
express=“1^0|0|1”,desired=false。
只有1^((0|0)|1)和1^(0|(0|1))的组合可以得到false。返回2。
express=“1”,desired=false。
没有组合可以得到false。返回0。
分析:这里给出递归版本和dp版本
递归版本:
bool isValid(const string &express){ int n = express.size(); if(!(n & 1)) return false; for(int i = 0; i < n; i += 2){ if(express[i] != '1' && express[i] != '0') return false; if(i+1 < n && express[i+1] != '|' && express[i+1] != '^' && express[i+1] != '&') return false; } return true; } int dfs(const string &express, bool desired, int l, int r){ if(l == r){ // 迭代的终止条件 if(desired) return express[l] == '1' ? 1 : 0; else return express[l] == '0' ? 1 : 0; } int res = 0; if(desired){ // 后续递归 for(int i = l+1; i <= r; i+=2){ switch(express[i]){ case '&': res += dfs(express, true, l, i-1) * dfs(express, true, i+1, r); break; case '|': res += dfs(express, true, l, i-1) * dfs(express, true, i+1, r); res += dfs(express, false, l, i-1) * dfs(express, true, i+1, r); res += dfs(express, true, l, i-1) * dfs(express, false, i+1, r); break; case '^': res += dfs(express, true, l, i-1) * dfs(express, false, i+1, r); res += dfs(express, false, l, i-1) * dfs(express, true, i+1, r); break; } } }else{ // 后续递归 for(int i = l+1; i <= r; i+=2){ switch(express[i]){ case '&': res += dfs(express, true, l, i-1) * dfs(express, false, i+1, r); res += dfs(express, false, l, i-1) * dfs(express, false, i+1, r); res += dfs(express, false, l, i-1) * dfs(express, true, i+1, r); break; case '|': res += dfs(express, false, l, i-1) * dfs(express, false, i+1, r); break; case '^': res += dfs(express, true, l, i-1) * dfs(express, true, i+1, r); res += dfs(express, false, l, i-1) * dfs(express, false, i+1, r); break; } } } return res; } int cattle10_2(const string &express, bool desired){ if(!isValid(express)) return 0; return dfs(express, desired, 0, express.size()-1); }
Dp版本:
// 动态规划 /* t[i][j]表示从i到j为true的方案总数 f[i][j]表示从i到j为false的方案总数 */ int cattle10_2_2(const string &express, bool desired){ if(!isValid(express)) return 0; int n = express.size(); int **t = new int*[n]; int **f = new int*[n]; for(int i = 0; i < n; i++){ t[i] = new int[n]; f[i] = new int[n]; memset(t[i], 0, sizeof(int)*n); memset(f[i], 0, sizeof(int)*n); } for(int i = 0; i < n; i+=2){ if(express[i] == '1')t[i][i] = 1; else f[i][i] = 1; } // 迭代条件 for(int i = 2; i < n; i+=2){ for(int j = i-2; j >=0; j-=2){ for(int k = j+1; k < i; k+=2){ switch(express[k]){ case '|': t[j][i] += t[j][k-1]*t[k+1][i]; t[j][i] += t[j][k-1]*f[k+1][i]; t[j][i] += f[j][k-1]*t[k+1][i]; f[j][i] += f[j][k-1]*f[k+1][i]; break; case '^': t[j][i] += t[j][k-1]*f[k+1][i]; t[j][i] += f[j][k-1]*t[k+1][i]; f[j][i] += f[j][k-1]*f[k+1][i]; f[j][i] += t[j][k-1]*t[k+1][i]; break; case '&': t[j][i] += t[j][k-1]*t[k+1][i]; f[j][i] += t[j][k-1]*f[k+1][i]; f[j][i] += f[j][k-1]*t[k+1][i]; f[j][i] += f[j][k-1]*f[k+1][i]; break; } } } } int result; if(desired) result = t[0][n-1]; else result = f[0][n-1]; for(int i = 0; i < n; i++){ delete []f[i]; delete []t[i]; } delete []f; delete []t; return result; }
4、排成一条线的纸牌博弈问题
【题目】
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
【举例】
arr=[1,2,100,4]。
开始时玩家A只能拿走1或4。如果玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A。如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A。玩家A作为绝顶聪明的人不会先拿4,因为拿了4之后玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。
arr=[1,100,2]。
开始时玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。
分析:这里同样给出递归版本和dp版本
递归版本
//递归做法 int pre_max(int *arr, int l, int r){ // 先选肯定是使自己获得最大值,在后选的两个中选最大的 if(l == r) return arr[l]; // 递归的终止条件 return max(post_min(arr, l+1, r)+arr[l], post_min(arr, l, r-1)+arr[r]); } int post_min(int *arr, int l, int r){ // 后选获得的对手让你得到最小值,在先选的两个中最小的 if(l == r) return 0; return min(pre_max(arr, l+1, r), pre_max(arr, l, r-1)); // 注意这里是最小不是最大 } int cattle10_3_1(int *arr, int n){ return max(pre_max(arr, 0, n-1), post_min(arr, 0, n-1)); // 先选和后选中的最大值 }
Dp版本:
/*动态规划的做法 F[i,j]表示从i到j先选所获得的钱数 f[i][j] = max(s[i+1][j]+arr[i], s[i][j-1]+arr[j]); S[i,j]表示从i到j后选所获得的钱数 s[i][j] = min(f[i+1][j],f[i][j-1]); 后取是最小值,两边都是绝顶聪明的人 */ int cattle10_3_2(int *arr, int n){ int **f = new int*[n]; // 表示从i到j先选所能获得的钱数 int **s = new int*[n]; // 表示从i到j后选获得的钱数 for(int i = 0; i < n; i++){ f[i] = new int[n]; s[i] = new int[n]; memset(f[i], 0, sizeof(int)*n); memset(s[i], 0, sizeof(int)*n); } for(int i = 0; i < n; i++) // 初始化 f[i][i] = arr[i]; for(int i = 1; i < n; i++){ // 迭代 for(int j = i-1; j>=0; j--){ f[j][i] = max(s[j][i-1]+arr[i], s[j+1][i]+arr[j]); s[j][i] = min(f[j][i-1], f[j+1][i]); } } /*for(int i = n-1; i >= 0; i--){ for(int j = i+1; j < n; j++){ f[i][j] = max(s[i+1][j]+arr[i], s[i][j-1]+arr[j]); s[i][j] = min(f[i+1][j],f[i][j-1]); } } */ int result = max(f[0][n-1], s[0][n-1]); for(int i = 0; i < n; i++){ delete []f[i]; delete []s[i]; } delete []f; delete []s; return result; }
1、如何仅用递归函数和栈操作逆序一个栈
【题目】
一个栈依次压入1,2,3,4,5那么从栈顶到栈底分别为5,4,3,2,1。将这个栈转置后,从栈顶到栈底为1,2,3,4,5,也就是实现栈中元素的逆序,但是只能用递归函数来实现,而不能用另外的数据结构。
【难度】
尉 ★★☆☆
分析:递归的过程函数本身就是用到栈数据结构,这里我们设计了两个递归函数,getLastElement是取得栈底元素并保持原有元素在栈中的顺序。
reverseStack是整体逆序栈的主函数。
代码:
/*仅仅使用递归算法使一个栈逆序*/ // 想想只有两个元素 代码是如何迭代的 int getLastElement(stack<int> &s){ int result = s.top(); s.pop(); if(s.empty()) return result; // 如果只有一个元素即为栈底的元素 返回 else{ int last = getLastElement(s); // 得到栈底元素 s.push(result); // 并栈顶元素压栈 return last; } } void reverseStack(stack<int> &s){ if(s.empty()) return; // 递归结束的终止条件 int i = getLastElement(s); // 得到栈底的最后一个元素 剩下元素依旧 reverseStack(s); // reverse 剩下 s.push(i); // 将i push进去 } void cattle11_1(stack<int> s){ reverseStack(s); while(!s.empty()){ cout << s.top() << " "; s.pop(); } cout << endl; }
2、判断一个链表是否为回文结构
【题目】
给定一个链表的头节点head,请判断该链表是不是回文结构。
例如:
1->2->1,返回true。
1->2->2->1,返回true。
15->6->15,返回true。
1->2->3,返回false。
进阶:
如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
分析:此题有三种方法,前两种方法的空间复杂度为O(N)。后一种方法的空间复杂度为O(1)
法一:遍历链表一遍,将元素压栈,然后再遍历链表一遍并且依次出栈与栈中元素比较
法二:设置一个快和慢指针,当快指针为空的时候,慢指针正好在链表的中间位置。然后慢指针继续向前,并将后半元素压栈。最后遍历链表一半元素与栈中元素比较即可。
法三:设置一个快和慢指针,当快指针为空的时候,慢指针正好在链表的中间位置。然后将后半链表逆序,然后首尾指针往中间移动进行遍历比较。
代码:
// O(N)时间的解法 O(N)空间复杂度 bool isPalindrome(Node *p){ stack<int> s; Node *q = p; while(q != NULL){ s.push(q->value); q = q->next; } q = p; while(!s.empty()){ if(s.top() == q->value){s.pop(); q= q->next;} else return false; } return true; } // 时间复杂度为O(N),空间复杂度为O(N/2) 注意查看链表长度为奇数或者偶数的情况 bool isPalindrome2(Node *p){ Node *q1= p, *q2 = p; while(q2 != NULL){ if(q2->next == NULL) break; q2 = q2->next->next; q1 = q1->next; } stack<int> s; while(q1 != NULL){ s.push(q1->value); q1 = q1->next; } q1 = p; while(!s.empty()){ if(s.top() == q1->value){s.pop(); q1 = q1->next;} else return false; } return true; } // 对链表进行逆序 Node* reverseList(Node *p){ Node *pre = NULL; Node *current = p, *post = p; while(current != NULL){ post = current ->next; current->next = pre; pre = current; current = post; } return pre; } // O(N)时间复杂度 O(1)空间复杂度 一个快指针 一个慢指针 // 当快指针结束的时候,慢指针刚好指向链表的中点 然后对后续半截进行逆序 然后对比 bool isPalindrome3(Node *p){ Node *q1= p, *q2 = p; while(q2 != NULL){ if(q2->next == NULL) break; q2 = q2->next->next; q1 = q1->next; } Node *r = reverseList(q1); q2 = p; while(r != NULL){ if(r->value == q2->value){ r = r->next; q2 = q2->next; } else return false; } return true; }
3、二叉树的序列化和反序列化
【题目】
二叉树被记录成文件的过程,叫做二叉树的序列化,通过文件内容重建原来二叉树的过程叫做二叉树的反序列化。给定一棵二叉树的头节点head,并已知二叉树节点值的类型为32位整型。请设计一种二叉树序列化和反序列化的方案并用代码实现。
分析:二叉树的序列化可以是前序中序或者后序序列化,序列化简单的可以看做将二叉树保存为一个字符串的形式(结点间用空格分割看来,空指针用#表示),反序列化可以由该字符串反转得到二叉树。
代码:
string intToString(int x){ stringstream ss; ss << x; string s; ss >> s; return s; } // 二叉树前序序列化 ---将二叉树保存到文件中 string seqTree(TNode *p){ // 递归 if(p == NULL){ // 递归结束的终止条件 return "# "; } string s; s += intToString(p->value) + " "; s += seqTree(p->left); // 后续左子树递归 s += seqTree(p->right); // 后续右子树递归 return s; } int stringToInt(string s){ stringstream ss; ss << s; int x; ss >> x; return x; } // 二叉树的反序列化 --- 将文件中字符串读出成为二叉树 void desTree(string &s, TNode *p){ int index = s.find(" "); /* if(index == string::npos){ // 迭代的最后如果没有空格的话 p = NULL; return; }*/ string str = s.substr(0, index); // 如果s只有1个#,那么返回的结果仍然是# s.substr(0,-1)返回的是元素本身***注意点 s = s.substr(index+1); if(str == "#"){ p = NULL; return; } p = new TNode(stringToInt(str)); desTree(s, p->left); desTree(s, p->right); }
一个极为重要的用途就是判断一个二叉树是否为另外一个二叉树的一部分,我们可以先对这两个二叉树进行序列化,然后用kmp算法判断一个字符串是否为另外一个字符串子串即可。时间复杂度为O(N)
代码:
// kmp算法求解一个字符串是否是另外一个字符串的子串 void getNext(int *next, int n, string p){ int j = 0; next[0]= -1; int k = -1; while(j < n-1){ if(k == -1 || p[j] == p[k]){ ++j; ++k; next[j] = k; } else k = next[k]; } } bool kmp(const string& str1, const string &str2){ int m = str1.size(); int n = str2.size(); int *next = new int[str2.size()]; getNext(next, n, str2); int i = 0, j = 0; while(i < m && j < n){ if( j== -1 || str1[i] == str2[j]){ i++; j++; } else j = next[j]; } delete []next; if(j == n) return true; else return false; } // 典型的应用就是求一个二叉树A是否为B树的子树 方法是先序列化,然后使用KMP算法判断A串是否为B串的字串 bool isTreeChildren(TNode *p1, TNode *p2){ string str1 = seqTree(p1); // 判断p2所指向的二叉树是否为p1所指向的二叉树的子树 string str2 = seqTree(p2); return kmp(str1, str2); }
1、两个单链表相交的一系列问题
单链表可能有环,也可能无环。给定两个单链表的头节点 head1和 head2, 这两个链表可能相交,也可能不相交。
请实现一个函数,如果两个链表相交,请返回相交的第一个节点;如果不相交,返回 null 即可。
要求:如果链表1的长度为N,链表2的长度为M,时间复杂度请达到O(N+M),额外空间复杂度请达到O(1)。
分析:
此题可以分解为很多小问题:
(1)首先就是如何判断一个链表是否有环及如果有环,如何返回入环的首结点。
法一是用hashmap,但是空间复杂度为O(N)
法二是设置一个快慢指针,快指针每次走两步,慢指针每次走一步,当快指针为空则表明无环,否则设置一个从头开始的结点,和当前相遇的结点,同时向前移动,当相遇时此时的结点即为入环的首结点--可以从数学上进行证明。
(2)其次判断两个链表是否相交,相交则返回相交的首结点。
分为三种情况
1:两个链表都没有环,方法是先分别求出两个链表的长度,然后让指向长链表的首指针先移动两链表长度的差值,然后同步移动,当相等时即为交点
2:其中一个有环,一个没有环,则绝对不可能有交点
3:两个都有环,分为两种情况,(1)在进环前就相交,转为1的情况 (2)进环后相交或者都有环不相交,只需第一个链表入环结点遍历一圈即可看是否等于第二个链表的入环结点
代码:
1:判断有无环,有环则返回入环首结点
// f1()判断链表是否有环,有换的话返回入环首结点,无环则返回空 /*法一是用hashmap,但是空间复杂度为O(N) 法二是设置一个快慢指针,快指针每次走两步,慢指针每次走一步,当快指针为空则表明无环, 否则设置一个从头开始的结点,和当前相遇的结点,同时向前移动,当相遇时此时的结点即为入环的首结点--可以从数学上进行证明 */ Node* f1(Node *head){ Node *p1=head, *p2= head; while(p2 != NULL && p2->next != NULL){ // 判断是否有环 p2 = p2->next->next; p1 = p1->next; if(p1 == p2) break; } if(p2 == NULL || p2->next == NULL) return NULL; Node *p3 = head; Node *p4 = p2; while(p3 != p4){ // 找到入环的首结点 p3 = p3->next; p4 = p4->next; } return p3; }
/* 判断两个链表是否相交,相交则返回相交的结点 分为三种情况: 1:两个链表都没有环,方法是先分别求出两个链表的长度,然后让指向长链表的首指针先移动两链表长度的差值,然后同步移动,当相等时即为交点 2:其中一个有环,一个没有环,则绝对不可能有交点 3:两个都有环,分为两种情况,(1)在进环前就相交,转为1的情况 (2)进环后相交或者都有环不相交,只需第一个链表入环结点遍历一圈即可看是否等于第二个链表的入环结点 */ // 两个链表都没有环,求出交点 Node *f2(Node *head1, Node *head2, Node *tail){ int l1 = 0, l2 = 0; Node *p1 = head1, *p2 = head2; while(p1 != tail){ l1++; p1 = p1->next; } while(p2 != tail){ l2++; p2 = p2->next; } p1 = head1; p2 = head2; int t = l1 - l2; if(l1 < l2){ p1 = head2; p2 = head1; t = l2 - l1; } while(t--) p1 = p1->next; while(p1 != tail && p2 != tail && p1 != p2){ p1 = p1->next; p2 = p2->next; } return p1; }
3:判断两个链表是否相交,相交则返回相交首结点
// 判断两链表是否相交及返回相交的交点 Node *f3(Node *head1, Node *head2){ Node *p1 = f1(head1); // 分别判断两链表是否有环 Node *p2 = f1(head2); // 一个有环一个无环 必不相交 if((p1 == NULL && p2 != NULL) || (p1 != NULL && p2 == NULL))return NULL; // 两个都无环 直接使用f2函数 if(p1 == NULL && p2 == NULL) return f2(head1, head2); // 进环前相交 if(p1 == p2) return f2(head1, head2, p1); Node *p3 = p1->next; while(p3 != p1){ if(p3 == p2) return p3; // 进环后相交 p3 = p3->next; } return NULL; // 都有环但是 不想交 }
2、设计 RandomPool结构
设计一种结构,在该结构中有如下三个功能:
1,insert(key):将某个key加入到该结构,做到不重复加入。
2,delete(key):将原本在结构中的某个key移除。
3,getRandom():等概率随机返回结构中的任何一个key。
要求:Insert、delete和 getRandom方法的时间复杂度都是 O(1)。
分析:此题的数据结构可以设置两个map和一个size,map对应keytoindex和indextokey,而关键在于使用随机洗牌的思想来删除元素,每次都是将待删除的元素与最后一个元素进行交换,再删除最后一个元素并且将size--。
代码:
/* 设置两个map和一个size,分别对应keyToIndex和indexToKey, 关键在于利用随机洗牌的原理进行删除 */ class RandomPool{ public: RandomPool():size(0){} void insert(int key){ if(keyToIndex.count(key)) return; keyToIndex[key] = size; indexToKey[size++] = key; } // 等概率获取某个键值 int getRandomKey(){ int x = rand()%size; return indexToKey[x]; } //关键在于删除元素为了保证等概率取得某个元素,随机洗牌的原理。 void deleteKey(int key){ if(keyToIndex.count(key))return; int value1 = keyToIndex[key]; int key2 = indexToKey[size-1]; keyToIndex[key2] = value1; indexToKey[value1] = key2; keyToIndex.erase(key); indexToKey.erase(size-1); size--; } private: map<int, int> indexToKey; map<int, int> keyToIndex; int size; };
1、生成窗口最大值数组
有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。
例如,数组为[4,3,5,4,3,3,6,7],窗口大小为3时:
[4 3 5] 4 3 3 6 7 窗口中最大值为5
4 [3 5 4] 3 3 6 7 窗口中最大值为5
4 3 [5 4 3] 3 6 7 窗口中最大值为5
4 3 5 [4 3 3] 6 7 窗口中最大值为4
4 3 5 4 [3 3 6] 7 窗口中最大值为6
4 3 5 4 3 [3 6 7] 窗口中最大值为7
如果数组长度为n,窗口大小为w,则一共产生n-w+1个窗口的最大值。请实现一个函数,给定一个数组arr,窗口大小w。返回一个长度为n-w+1的数组res,res[i]表示每一种窗口状态下的最大值。以本题为例,结果应该返回[5,5,5,4,6,7]。
分析:获取滑动窗口内的最大值,暴力解决方法时间复杂度O(nw)
采用一个双端队列,时间复杂度可以降低为O(n),入队列的是数组的下标,元素较小则入队列,较大则弹出较小的元素(队列按照由大到小的顺序逐渐递减),
取最大值前要判断队首元素是否过期,过期则弹出队首元素
注意求子数组的最大值和最小值要想到双端队列
代码:
int* cattle13_1(int *arr, int n, int w){ deque<int> deq; int *res = new int[n-w+1]; int index = 0; for(int i = 0; i < n; i++){ while(!deq.empty() && arr[deq.back()] <= arr[i]) // 队列不为空且队尾元素小于arr[i] 则弹出队尾元素 deq.pop_back(); deq.push_back(i); if(i - deq.front() == w)deq.pop_front(); // 判断队首元素是否过期 if(i >= w-1)res[index++] = arr[deq.front()]; } return res; }
2、最大值减去最小值小于或等于num的子数组数量
给定数组arr和整数num,返回有多少个子数组满足如下情况:
max(arr[i..j]) - min(arr[i..j]) <=num
max(arr[i..j])表示子数组arr[i..j]中的最大值,min(arr[i..j])表示子数组arr[i..j]中的最小值。如果数组长度为 N,请实现时间复杂度为 O(N)的解法。
分析:求出arr[i,j]中最大值减去最小值小于等于num的子数组的数目
有两个性质
(1)若arr[i,j]不满足要求,那么arr[i,j]的扩展数组也绝对不会满足要求
(2)若arr[i,j]满足要求,那么arr[i,j]的子数组也绝对会满足要求
deq_max保存子数组arr[i,j]的最大值并且是递减的
deq_min保存子数组arr[i,j]的最小值并且是递增的
时间复杂度:虽然有多重循环,但是i和j都没有退回,每次j停止,i最多向前移动到j位置,故为O(2*N)
内部的元素入队列和出队列也只是n次,故时间复杂度仍然为O(N).
代码:
int cattle13_2(int *arr, int n , int num){ int res = 0; // 保存子数组的个数 deque<int> deq_max, deq_min; int j = 0; for(int i = 0; i < n; i++){ for(; j < n; j++){ while(!deq_max.empty() && arr[deq_max.back()] <= arr[j]) deq_max.pop_back(); deq_max.push_back(j); while(!deq_min.empty() && arr[deq_min.back()] >= arr[j]) deq_min.pop_back(); deq_min.push_back(j); if(arr[deq_max.front()]- arr[deq_min.front()]> num) break; } if(deq_max.front() == i) deq_max.pop_front(); if(deq_min.front() == i) deq_min.pop_front(); res += j - i; // j-i就是子数组的数量 } return res; }
3、复制含有随机指针节点的链表
一种特殊的链表节点类描述如下:
class Node {
public int value;
public Node next;
public Node rand;
public Node(int data) {
this.value = data;
}
}
Node类中的value是节点值,next指针和正常单链表中next指针的意义一样,都指向下一个节点,rand指针是Node类中新增的指针,这个指针可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。要求除了需要返回的东西外,不再使用额外的数据结构并且在时间复杂度为 O(N)内完成原问题要实现的函数。
分析:这道题有两种方法:
法一:hash方法,需要额外的空间复杂度为O(N)。方法是先copy原来的链表,并且用两个map保存链表的对应关系:
map1 新到旧,map2 旧到新
然后遍历新链表,由map1得到对应的旧链表结点,然后找到其rand指针,然后由map2找到rand指针结点对应的结点。
法二:copy一个新的链表,使结构变为1->1'->2->2'->3->3' 然后遍历每次取出两个结点,得知1的rand指向的结点,
然后使1'的rand的结点指向1的rand的结点的下一个结点即可,其它节点类似。然后恢复原有的结构
4、一种怪异的节点删除方式
链表节点值类型为int型,给定一个链表中的节点node,但不给定整个链表的头节点,如何在链表中删除node?请实现这个函数,并分析这么会出现哪些问题。要求时间复杂度为O(1)。
分析:方法是1->2->3,如删除2,可以将3复制给2变成1->3->3, 然后再删除3,得到1->3(实质是删除了当前结点的后一个结点)
此方法存在的问题有:
(1)如果node指向的就是最后一个结点,则将无法删除
(2)可能外部对每个结点都有依赖,上述方法则会有问题
5、设计可以变更的缓存结构
【题目】
设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:
1,set(key,value):将记录(key,value)插入该结构。
2,get(key):返回key对应的value值。
【要求】
1,set和get方法的时间复杂度为O(1)。
2,某个key的set或get操作一旦发生,认为这个 key 的记录成了最经常使用的。
3,当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
【举例】
假设缓存结构的实例是cache,大小为3,并依次发生如下行为:
1,cache.set("A",1)。最经常使用的记录为("A",1)。
2,cache.set("B",2)。最经常使用的记录为(“B”,2),(“A”,1)变为最不经常的。
3,cache.set("C",3)。最经常使用的记录为(“C”,2),(“A”,1)还是最不经常的。
4,cache.get("A")。最经常使用的记录为(“A”,1),(“B”,2)变为最不经常的。
5,cache.set("D",4)。大小超过了 3,所以移除此时最不经常使用的记录(“B”,2),加入记录(“D”,4),并且为最经常使用的记录,然后("C",2)变为最不经常使用的记录。
分析:采用双端链表的数据结构,因为要快速地移动一个元素,另外需要两个hash表(主要是为了查找和删除),
保存键值到结点值和结点值到键值的对应关系
代码:
头文件:
#ifndef CATTLE13_H #define CATTLE13_H #include <deque> #include <map> #include <string> using namespace std; int* cattle13_1(int *arr, int n, int w); int cattle13_2(int *arr, int n , int num); struct DoubleList{ DoubleList *next, *last; int value; DoubleList(int v):value(v), next(NULL), last(NULL){} }; class Cache{ private: DoubleList*head, *tail; map<string, DoubleList* > key_to_node; map<DoubleList*,string> node_to_key; void move_to_last(DoubleList* node); // 将节点移至双端链表的末尾 void add_node(DoubleList*node); // 在双端链表的末尾增加结点 int capacity; static int cache_count; public: Cache():head(NULL), tail(NULL), capacity(0){} void set(string s, int value); int get(string s); void display(){ DoubleList *p = head; while(p != NULL){ cout << p->value << " "; p = p->next; } cout << endl; } }; #endifcpp文件
int Cache::cache_count=3; void Cache::move_to_last(DoubleList* node){ if(node == tail) return; if(node == head){ head = head->next; head->last = NULL; } else{ node->last->next = node->next; node->next->last = node->last; } tail->next = node; node->last = tail; node->next = NULL; tail = node; } void Cache::add_node(DoubleList*node){ capacity++; if(head == NULL){head = node; tail = node; return;} // 原来就为空 tail->next= node; node->last = tail; node->next = NULL; tail = node; if(capacity > cache_count){ // 是否超出缓存容量 DoubleList*del_node = head; head = head->next; head->last = NULL; string key = node_to_key[del_node]; node_to_key.erase(del_node); key_to_node.erase(key); delete del_node; } } void Cache::set(string s,int value){ DoubleList *new_node = new DoubleList(value); if(key_to_node.count(s)){ // 已经存在 DoubleList *node = key_to_node[s]; key_to_node[s] = new_node; move_to_last(new_node); node_to_key.erase(node); node_to_key[new_node] = s; } else{ // 新加入结点 add_node(new_node); key_to_node[s] = new_node; node_to_key[new_node] = s; } } int Cache::get(string s){ // 得到键值 DoubleList *node = key_to_node[s]; move_to_last(node); return node->value; }
题目一最长的可整合子数组的长度
先给出可整合数组的定义。如果一个数组在排序之后,每相邻两个数差的绝对值都为1,则该数组为可整合数组。例如,[5,3,4,6,2]排序之后为[2,3,4,5,6],符合每相邻两个数差的绝对值都为1,所以这个数组为可整合数组。给定一个整型数组arr,请返回其中最大可整合子数组的长度。例如,[5,5,3,2,6,4,3]中最大可整合子数组为[5,3,2,6,4],所以返回5。
分析:判断一个子数组是否为可整合,只需用hset判断是否有重复元素,无重复元素判断最大值减去最小值是否等于数组长度即可
如果用暴力求解的话,求出每个子数组时间复杂度为O(N^2),再排序判断为O(NlgN),故总的时间复杂度为O(N^3*lgN)
而采用hash_set所需要的时间复杂度为O(N^2),每次遍历一个子数组,O(1)时间进行判断
代码:
int cattle14_1(int *arr, int n){ if(n <= 1) return 0; unordered_set<int> hset; int len = 0; int max_val = arr[0]; int min_val = arr[0]; for(int i = 0; i < n; i++){ for(int j = i; j < n; j++){ // 遍历每一个子数组, if(hset.count(arr[j]))break; // 重复则该子数组不是 max_val = max(max_val, arr[j]); min_val = min(min_val, arr[j]); if(max_val - min_val == j-i) // 不重复 且满足要求--可整合子数组 len = max(len, j-i+1); hset.insert(arr[j]); } hset.clear(); } return len; }
题目二子数组的最大累加和问题
给定一个数组arr,返回子数组的最大累加和。例如,arr=[1,-2,3,5,-2,6,-1],所有的子数组中,[3,5,-2,6]可以累加出最大的和12,所以返回12。
分析:子数组最大累加和 dp问题,每次求取以第i个元素结尾的最长连续子数组的和tmp,然后求取全局最大值res
代码:
int cattle14_2(int *arr, int n){ int res = arr[0]; int tmp = arr[0]; for(int i = 1; i < n; i++){ tmp = tmp > 0 ? tmp+arr[i]:arr[i]; res = max(tmp, res); } return res; }
题目三 找到两个不相容子数组的最大和
给定一个数组arr,其中有很多的子数组,找到两个不相容子数组使得相加的和最大,并返回和的最大值。比如,数组[1,-1,0,-2,3,5,-2,8,7,-4],两个不相容子数组分别为[3,5]和[8,7]时累加和最大,所以返回23。再比如,数组[3,-1,0,-2,3,5,-2,8,7,-4],两个不相容子数组分别为[3]和[3,5,-2,8,7]时累加和最大,所以返回24。
分析:采用两个预处理数组。时间复杂度为O(N),空间复杂度为O(N)
l[i]用来表示arr[0..i]这个子数组最大累加和
r[i]用来表示arr[i..n-1]这个子数组的最大累加和
然后遍历一遍l[i]+r[i+1]的全局最大值即可
代码:
/*采用两个预处理数组。时间复杂度为O(N),空间复杂度为O(N) l[i]用来表示arr[0..i]这个子数组最大累加和 r[i]用来表示arr[i..n-1]这个子数组的最大累加和 然后遍历一遍l[i]+r[i+1]的全局最大值即可 */ int cattle14_3(int *arr, int n){ int *l = new int[n]; int *r = new int[n]; l[0]= arr[0]; int tmp = arr[0]; for(int i = 1; i < n; i++){ tmp = tmp > 0 ? tmp+arr[i]:arr[i]; l[i] = max(l[i-1], tmp); } tmp = arr[n-1]; r[n-1] = arr[n-1]; for(int i = n-2; i >= 0; i--){ tmp = tmp > 0 ? tmp+arr[i] : arr[i]; r[i] = max(r[i+1], tmp); } int res = min_int; for(int i = 0; i < n-1; i++){ res = max(res, l[i]+r[i+1]); } delete []l; delete []r; return res; }
题目四 子矩阵的最大累加和问题
给定一个矩阵matrix,其中的值有正、有负、有0,返回子矩阵的最大累加和。例如,矩阵matrix为:
-90 48 78
64 -40 64
-81 -7 66
其中,最大累加和的子矩阵为:
48 78
-40 64
-7 66
所以返回累加和209。
再例如,matrix为:
-1 -1 -1
-1 2 2
-1 -1 -1
其中最大累加和的子矩阵为:
2 2
所以返回累加和 4。
分析:此题采用以行为单位求取子数组
代码:
int getArrSum(vector<int> v){ int res = v[0]; int tmp = v[0]; for(int i = 1; i < v.size(); i++){ tmp = tmp > 0 ? tmp+v[i] : v[i]; res = max(tmp, res); } return res; } int cattle14_4(vector<vector<int> > matrix){ int m = matrix.size(); int n = matrix[0].size(); vector<int> tmp; tmp.resize(n, 0); int res = matrix[0][0]; for(int i = 0; i < m; i++){ tmp.assign(n, 0); for(int j = i; j < m; j++){ for(int k = 0; k < n; k++) tmp[k] += matrix[j][k]; res = max(res, getArrSum(tmp)); } } return res; }
题目五
给定一个无序数组arr,其中元素可正、可负、可0,再给定一个整数k,求arr所有的子数组中累加和为k的最长子数组长度。
分析:无序数组,arr,可正可负可0,求最大累加和为k的子数组的长度
时间复杂度O(N),需要一个hash_map结构,空间复杂度为O(N)
变形1:如果数组元素全部为正数,则时间复杂度为O(N),空间复杂度为O(1)——滑动窗口
变形2:如果求最大累加和为小于或者等于k的子数组的长度,则时间复杂度为O(N),空间复杂度为O(NlgN)
代码:
/* 无序数组,arr,可正可负可0,求最大累加和为k的子数组的长度 时间复杂度O(N), 需要一个hash_map结构,空间复杂度为O(N) 变形1:如果数组元素全部为正数,则时间复杂度为O(N),空间复杂度为O(1)——滑动窗口 变形2:如果求最大累加和为小于或者等于k的子数组的长度,则时间复杂度为O(N),空间复杂度为O(NlgN) */ int cattle14_5(int *arr, int n, int k){ unordered_map<int , int> hmap; hmap.insert(pair<int, int>(0,-1)); // 这个很重要 首元素包括在内了 int res = 0; int sum = 0; for(int i = 0; i < n; i++){ sum += arr[i]; if(hmap.count(sum-k)){ // 判断之前是否已经有累加和等于sum-k,那么所对应的下标+1到i就是满足条件的子数组 res = max(res, i-hmap[sum-k]); } if(!hmap.count(sum)) hmap[sum] = i; } return res; }
题目六
给定一个无序数组arr,其中元素可正、可负、可0,求arr所有的子数组中正数与负数个数相等的最长子数组长度。
分析:问题等价于将正数变为1,负数变为-1,然后就是求取最大累加和等于0的子数组的长度
代码:
/* 问题等价于将正数变为1,负数变为-1,然后就是求取最大累加和等于0的子数组的长度 */ int cattle14_6(int *arr, int n){ for(int i = 0; i < n; i++){ if(arr[i] < 0) arr[i] = -1; else if(arr[i] > 0) arr[i] = 1; } return cattle14_5(arr, n, 0); }
题目七
给定一个无序数组arr,其中元素只是1或0,求arr所有的子数组中0和1个数相等的最长子数组长度。
分析:问题等价于将数组中0变为-1,然后求取最大累加和等于0的子数组的长度
代码:
/* 问题等价于将数组中0变为-1,然后求取最大累加和等于0的子数组的长度 */ int cattle14_7(int *arr, int n){ for(int i = 0; i < n; i++) if(arr[i] == 0) arr[i] = -1; return cattle14_5(arr, n, 0); }
题目一
有一个机器按自然数序列的方式吐出球(1 号球,2 号球,3 号球,......),你有一个袋子,袋子最多只能装下K个球,并且除袋子以外,你没有更多的空间。设计一种选择方式,使得当机器吐出第N号球的时候(N>K),你袋子中的球数是K个,同时可以保证从1号球到N号球中的每一个,被选进袋子的概率都是K/N。举一个更具体的例子。有一个只能装下10个球的袋子,当吐出100个球时,袋子里有10个球,并且1~100号中的每一个球被选中的概率都是10/100。然后继续吐球,当吐出1000个球时,袋子里有10个球,并且1~1000号中的每一个球被选中的概率都是10/1000。继续吐球,当吐出i个球时,袋子里有10个球,并且1~i号中的每一个球被选中的概率都是10/i,即吐球的同时,已经吐出的球被选中的概率也动态地变化。
分析:
/*策略是当吐出第i个球时,调用随机函数产生1~i的一个整数,如果该整数在k+1~i之间,则丢弃该数;否则再产生1~k之间的一个随机数,并且用第i号球替换它
这样当吐出N个球的时候,每个球被保留的概率都为K/N——可以证明的
证明:吐出了N个球,求第a球被保留的概率
(1)当a球在1~k之间时,吐出第k个球时,a被保留的概率为1,;吐出第k+1个球时,a被保留的概率为k/k+1;
吐出第k+2个球时,a被保留的概率为k+1/k+2;...吐出第N个球时,a被保留的概率为N-1/N;
所以a被保留的概率就是1*(k)/(k+1)*(k+1)/(k+2)*...*N-1/N= K/N
(2)当a球在k+1~N之间时,吐出第a个球时,a被保留的概率是k/a,;吐出第a+1个球时,a被保留的概率为a/a+1;
吐出第a+2个球时,a被保留的概率为a+1/a+2;...吐出第N个球时,a被保留的概率为N-1/N;
所以a被保留的概率就是k/a*a/a+1*...*N-1/N= K/N
这样在实际应用如抽奖活动中,只需要保持大小为K的中奖池,不管多少人来了,采取这样的策略每个人中奖的概率都是K/N(这样就可以保持N个用户去随机选择K个中奖用户了)
*/
代码:
int* cattle16_1(int N, int K){ int *arr = new int[K]; for(int i = 0; i < K; i++){ arr[i] = i+1; } for(int i = K; i < N; i++){ int x = rand()%(i+1); if(x < K){ x = rand()%K; arr[x] = i+1; } } return arr; }
题目二
给定字符串str,其中绝对不含有字符’.’和’*’。再给定字符串exp,其中可以含有’.’或’*’,’*’字符不能是exp的首字符,并且任意两个’*’字符不相邻。exp中的’.’代表任何一个字符,exp中的’*’表示’*’的前一个字符可以有0个或者多个。请写一个函数,判断str是否能被exp匹配。
【举例】
str=“abc”,exp=“abc”。返回true。
str=“abc”,exp=“a.c”。exp中单个’.’可以代表任意字符,所以返回true。
str=“abcd”,exp=“.*”。exp中’*’的前一个字符是’.’,所以可表示任意数量的’.’字符,所以当exp是“....”时与“abcd”匹配,所以返回true。
str=“”,exp=“..*”。exp中’*’的前一个字符是’.’,可表示任意数量的’.’字符,但是”.*”之前还有一个’.’字符,该字符不受‘*’的影响,所以str起码得有一个字符才能被exp匹配。所以返回false。
分析:此题为狂难题,可以采用暴力递归和动态规划的方法
递归大致可以分为两种情况:
1:ei+1的值不等于*,那么判断si的值与ei的值是否相等相等继续否则返回false
2:ei+1的值等于*,那么判断si的值与ei的值是否相等,*可以循环替代ei值0,1,2等个,不相等直接跳过
代码:
bool isValid(const string &str, const string &exp){ for(int i = 0; i < str.size(); i++){ if(str[i] == '.' || str[i] == '*')return false; } if(exp[0] == '*') return false; for(int i = 0; i < exp.size()-1; i++){ if(exp[i] == '*' && exp[i+1] == '*') return false; } return true; } /*递归大致可以分为两种情况: 1:ei+1的值不等于*,那么判断si的值与ei的值是否相等 相等继续 否则返回false 2:ei+1的值等于*,那么判断si的值与ei的值是否相等,*可以循环替代ei值0,1,2等个,不相等直接跳过 */ bool process(const string &s, const string &e, int si, int ei){ if(ei == e.size()) return si == s.size(); if(ei+1 == e.size() || e[ei+1] != '*'){ // ei为最后一个元素或者ei+1不为* if(si < s.size() && (s[si] == e[ei] || e[ei]=='.')) return process(s, e, si+1, ei+1); // 注意这里判断si是否越界 else return false; } // 注意判断si是否越界 while(si < s.size() && (s[si] == e[ei] || e[ei]=='.')){ // ei不是最后一个元素且ei+1值为* 且ei的值等于si的值情况,*号替代si 0, 1,2 次情况 if(process(s, e, si, ei+2)) return true; si++; } return process(s, e, si, ei+2); // ei的值不等于si的值情况 此时*号只能替代0个ei值 } bool cattle16_2(string s, string e){ if(!isValid(s,e)) return false; return process(s, e, 0, 0); }
dp分析:si,ei位置的值只与[si+1,ei+1],[si,ei+2], [si+1, ei+2].. 隔一列的行有关
所以动态规划的初始化要初始化dp的最后一行,最后两列---其中每个字符串最后多加了一个结束符 dp[m+1][n+1]
然后内部遍历,当e[ei+1]!='*'和=='*'两步进行处理。。
代码:
/*si,ei位置的值只与[si+1,ei+1], [si,ei+2], [si+1, ei+2].. 隔一列的行有关 所以动态规划的初始化要初始化dp的最后一行,最后两列---其中每个字符串最后多加了一个结束符 dp[m+1][n+1] 然后内部遍历,当e[ei+1]!='*'和=='*'两步进行处理。。 */ bool cattle16_2_dp(const string &s, const string &e){ if(!isValid(s, e)) return false; int m = s.size(); int n = e.size(); bool **dp = new bool*[m+1]; for(int i = 0; i <= m; i++){ dp[i] = new bool[n+1]; memset(dp[i], 0, sizeof(bool)*(n+1)); } dp[m][n] = true; for(int i = n-2; i >=0; i -= 2){ if(e[i+1] == '*')dp[m][i] = true; else break; } if(m-1>= 0 && s[m-1] == e[n-1] || e[n-1] == '.')dp[m-1][n-1]= true; for(int i = m-1; i >= 0; i--){ for(int j = n-2; j >= 0; j--){ if(e[j+1] != '*'){ // 不等于*号时 if(s[i] == e[j] || e[j]=='.')dp[i][j] = dp[i+1][j+1]; }else{ // 等于*号的情况 int k = i; while(k != m && (s[k] == e[j] || e[j]=='.')){ if(dp[k][j+2]){ dp[i][j]=true; break;} k++; } if(!dp[i][j]) dp[i][j] = dp[k][j+2]; } } } bool result = dp[0][0]; for(int i = 0; i <= m; i++) delete []dp[i]; delete []dp; return result; }
题目三
给定字符串str1和str2,求str1的子串中含有str2所有字符的最小子串长度。
【举例】
str1="abcde”,str2="ac"。因为"abc"包含str2的所有字符,并且在满足这一条件的str1的所有子串中,”abc"是最短的,所以返回3。str1="12345”,str2="344"。最小包含子串不存在,返回0。
分析:滑动窗口题
代码:
/*滑动窗口*/ int cattle16_3(const string &str1, const string &str2){ int m = str1.size(); int n = str2.size(); map<char , int> hmap; // 用来记录str2中每个字符出现的次数 for(int i = 0; i < n; i++){ hmap[str2[i]]++; } int let_counts = 0; // 用来标记窗口是否满足要求 map<char, int> windows; int slow = 0; int min_len = m+1; for(int fast = 0; fast < m; fast++){ char c = str1[fast]; if(hmap.find(c) != hmap.end()){ windows[c]++; if(windows[c] <= hmap[c]){ let_counts++; } if(let_counts >= n){ // 窗口满足要求了 while(hmap.find(str1[slow]) == hmap.end() || windows[str1[slow]] > hmap[str1[slow]]){ // 窗口缩减 windows[str1[slow]]--; slow++; } min_len = min(min_len, fast-slow+1); windows[str1[slow]]--; // 窗口缩减 slow++; let_counts--; // 重要 } } } return min_len; }
题目四
给定一个长度不小于2的数组 arr,实现一个函数调整arr,要么让所有的偶数下标都是偶数,要么让所有的奇数下标都是奇数。如果 arr 的长度为 N,函数要求时间复杂度为 O(N),额外空间复杂度为 O(1)。
分析:方法是i指向偶数下标, j指向奇数下标,判断最后一个元素是奇数还是偶数,如果是偶数 则与i指向元素互换,i+=2; 否则与j指向的元素互换,j+=2
/* 方法是i指向偶数下标, j指向奇数下标,判断最后一个元素是奇数还是偶数,如果是偶数 则与i指向元素互换,i+=2; 否则与j指向的元素互换,j+=2 */ void cattle16_4(int *arr, int n){ if(n <= 1)return; int i = 0, j = 1; while(i < n || j < n){ if(arr[n-1]%2 == 0){ swap(arr[n-1], arr[i]); i += 2; } else{ swap(arr[n-1], arr[j]); j += 2; } } }
参考文献:http://www.nowcoder.com/live/courses