1.
给定一个无序的整型数组,求出最小的k个数
两种思路:
(1)如果所有的数组可以全部装入内存的话,采用快速排序的思想进行划分,不断向第k个小的数靠近,当得到第k小的数(key)时就相当于得到了最小的k个数,因为划分的时候保证了比key小的数全部放在了左边。平均时间复杂度为O(N)。
(2)当数组太大无法一次性装入内存时,上面的方法失效,可以维持一个大小为k的大顶堆,取前k个数建堆后依次读取之后的n-k个数,当数字比堆顶的元素小的话就更新堆,否则直接丢弃,扫描完数组后堆中即保留了前k小的数字,时间复杂度为O(Nlogk)。
下面的代码时划分的思路:
void partition(vector<int> &num, int left, int right, int k)
{
if(left >= right) return;
int pivot = num[right];
int l = left - 1, r;
for(r = left; r < right; ++r)
{
if(num[r] <= pivot)
{
++l;
swap(num[l], num[r]);
}
}
swap(num[l+1], num[right]);
if(k == l + 1) return;
else if(k < l+1)
{
partition(num, left, l, k);
} else {
partition(num, l+2, right, k);
}
}
2.
把数组排成以字典序来看的最小数字
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
主要思路:
本题主要是考虑如何定义规则(数字间的比较规则),使得排列得到的数组为字典序最小。
bool myCmp(int a, int b)
{
char buffer[32];
sprintf(buffer, "%d", a);
string sa = buffer;
sprintf(buffer, "%d", b);
string sb = buffer;
string sab = sa + sb;
string sba = sb + sa;
return (sab.compare(sba) < 0);
}
sort(num.begin(), num.end(), myCmp);
3.
求数组中的逆序对的数目(扩展到基因上O(N)的时间)
主要思路:通过归并排序计算逆序数,当两个有序数组进行合并时,很容易得到对于后面的有序数组的每个数,前面有多少个数字比它大(逆序对)
void MergeSorted(vector<int> &num, int left, int right, long long &cnt)
{
int mid = left + ((right - left) >> 1);
vector<int> tmp(right - left + 1, 0);
int idx = 0, i = left, j = mid + 1;
while(i <= mid && j <= right)
{
if(num[i] <= num[j])
{
tmp[idx++] = num[i++];
} else {
cnt += (mid - i + 1); // counting
tmp[idx++] = num[j++];
}
}
while(i <= mid)
{
tmp[idx++] = num[i++];
}
while(j <= right)
{
tmp[idx++] = num[j++];
}
for(i = left; i <= right; ++i)
{
num[i] = tmp[i - left];
}
}
void MergeSortReversCnt(vector<int> &num, int left, int right, long long &cnt)
{
if(left < right)
{
int mid = left + ((right - left) >> 1);
MergeSortReversCnt(num, left, mid, cnt);
MergeSortReversCnt(num, mid + 1, right, cnt);
MergeSorted(num, left, right, cnt);
}
}
变型(腾讯2015年校招笔试):
今年腾讯校园招聘的笔试一道填空题对本题进行了一点扩展,就可以完全换一种思路。
同样是求逆序对的数目,如果我们需要处理的数组是基因中的碱基ACGT,可以用O(N)的时间解决该问题。
解决方法是用c[4] = {0, 0, 0, 0}记录四个碱基的数目,当访问到第i个元素时,分四种情况考虑:
case 'A': cnt += (c[1] + c[2] + c[3] ); ++c[0]; break;
case 'C': cnt += ( c[2] + c[3] ); ++c[1]; break;
case 'G': cnt += c[3] ; ++c[2]; break;
case 'T': ++c[3]; break;
4.
数字在排序数组中出现的次数
主要思路:两次二分搜索,分别查找上边界和下边界
int BinSearchLower(vector<int> &num, int target)
{
int left = 0, right = (int) num.size() - 1;
while(left <= right)
{
int mid = left + ((right - left) >> 1);
if(num[mid] < target) left = mid + 1;
else right = mid - 1;
}
if(left < num.size() && num[left] == target)
return left;
else return -1;
}
int BinSearchHigher(vector<int> &num, int target)
{
int left = 0, right = (int) num.size() - 1;
while(left <= right)
{
int mid = left + ((right - left) >> 1);
if(num[mid] <= target) left = mid + 1;
else right = mid - 1;
}
return right;
}
int countInSortedArr(vector<int> &num, int target)
{
if(0 == num.size()) return 0;
int l = BinSearchLower(num, target);
if(-1 == l) return 0;
int r = BinSearchHigher(num, target);
return (r - l + 1);
}
5.
数组中只出现一次的数字(几种变型)
(1)最简单的情况,数组共2k+1个元素,只有一个数字只出现了一次,求出这个数字。(直接用异或运算)
int singleNumber(int A[], int n)
{
int ans = 0;
for (int i = 0; i < n; i++) ans ^= A[i];
return ans;
}
其实还有一种方法,就是利用快速排序的划分,每次丢弃一半的数字,写起来复杂很多,但平均时间复杂度也是O(N)。
int partition(int A[], int left, int right)
{
int i = left + 1;
int j = right - 1;
int temp, pos = -1;
while(i <= j)
{
while(i <= j && A[i] <= A[left])
{
if(A[i] == A[left]) pos = i;
++i;
}
while(i <= j && A[j] > A[left]) --j;
if(i > j) break;
swap(A[i], A[j]);
}
swap(A[i-1], A[left]);
if(pos != -1 && i > left + 1)
{
swap(A[pos], A[i-2]);
}
return i;
}
int QuickPick(int A[], int left, int right)
{
if(left+1 == right) return A[left];
if(left + 3 == right)
{
if(A[left] != A[left+1] && A[left] != A[left+2])
return A[left];
else if(A[left] != A[left+1]) return A[left+1];
else return A[left+2];
}
int pivot = partition(A, left, right);
if((pivot-left) % 2 == 1)
{
return QuickPick(A, left, pivot);
} else {
return QuickPick(A, pivot, right);
}
}
int singleNumber(int A[], int n)
{
return QuickPick(A, 0, n);
}
(2)数组中有3k+1个数字,只有1个数字出现一次,其他的都出现3次。
对于32位的整数,可以数每个位置上为1的数字的个数。
int singleNumber(int A[], int n)
{
int count = 0;
int result = 0;
for (int i = 0; i < 32; i++)
{
count = 0;
for (int j = 0; j < n; j++)
{
count += ((A[j] >> i) & 1);
}
result |= ((count % 3) << i);
}
return result;
}
其实(1)中的划分方法依旧适用,只是每次丢弃3x个数字,leetcode上这题采用划分的方法也可以AC,跟上面一样看起来很复杂!
(3)(雅虎北研2015年校招笔试)数组共4k+2个元素,只有一个数字出现了2次,其他的都出现了4次,求出现2次的这个数字。
处理方法与(2)类似,依次数每位上n个数字中出现的1的数目,如果是4k个,目标值该位上就是0,如果是4k+个,那目标值该位上为1, T(n) = 32n。但是快排的划分思想也可以很好的运用。
每次选择一个主元pivot进行划分后,让其左边都是比pivot小的,右边都是不小于pivot的,确定
pivot位置后。
就需要看划分后的左边部分和右边部分(包括pivot)元素的数目,其中肯定有一部分是4x个元素,另外一部分是4y+2个元素,下次划分我们只需要在4y+2那部分去找了。但是有一个需要注意的问题是左边的4x可能是0,这样划分下去就死循环了,因此我们选主元的时候可以随机选,或者取left, right, mid三个元素选择居中的。
(4)数组共2k+2个元素,有两个数字都是只出现了一个,其他都是两次。求这两个数字。
主要思路:先求出所有数的异或结果, 找出最后一个1的位置, 根据该位是否为1把num数组分成两个部分,这两部分分别异或就得到要求的两个数字。
void FindNumsAppearOnce(vector<int> &num, int &num1, int &num2)
{
if(0 == num.size()) return;
// 先求出两个目标数的异或结果
int retOR = 0;
for(size_t i = 0; i < num.size(); ++i)
{
retOR ^= num[i];
}
// 找出最后一个1的位置,最低位记为第0位
unsigned int last1Idx = 0;
while(0 == (retOR & 1) && last1Idx < 8 *sizeof(int))
{
retOR >>= 1;
++last1Idx;
}
// 根据last1Idx位是否为1把num数组分成两个部分,这两部分分别异或就得到要求的两个数字
for(size_t i = 0; i < num.size(); ++i)
{
if(((num[i] >> last1Idx) & 1))
{
num1 ^= num[i];
} else num2 ^= num[i];
}
}
6.
顺时针打印矩阵
主要思路:直接打印,考虑清楚边界
void printMatrix(vector<vector<int> > &matrix, int m, int n)
{
int r = n - 1;
int d = m - 1;
int len = 1 + (m < n ? m : n);
len /= 2;
for(int s = 0; s < len; ++s)
{
for(int j = s; j <= r; ++j)
cout << matrix[s][j] << " ";
for(int i = s + 1; i <= d; ++i)
cout << matrix[i][r] << " ";
if(d > s)
{
for(int j = r - 1; j >= s; --j)
cout << matrix[d][j] << " ";
}
if(r > s)
{
for(int i = d - 1; i > s; --i)
cout << matrix[i][s] << " ";
}
--r;
--d;
}
cout << endl;
}
7.
删除有序数组中的重复数字
见leetcode中的 Remove Duplicates from Sorted Array(有序数组,重复的元素只保留一个)
int removeDuplicates(int A[], int n) {
int cnt = 0;
if(n == 0) return cnt;
int elem = A[0];
++cnt;
for(int i = 1; i < n; ++i)
{
if(A[i] != elem)
{
if(cnt != i) A[cnt] = A[i];
elem = A[i];
++cnt;
}
}
return cnt;
}
扩展版本, Remove Duplicates from Sorted Array II(有序数组,重复的元素最多保留2个)
int removeDuplicates(int A[], int n) {
int cnt = 0;
if(0 == n) return cnt;
A[cnt++] = A[0];
int appeared = 1;
for(int i = 1; i < n; ++i)
{
if(A[i] == A[cnt-1] && appeared == 2)
{
continue;
} else if(A[i] == A[cnt-1] && appeared < 2) {
A[cnt++] = A[i];
++appeared;
} else {
A[cnt++] = A[i];
appeared = 1;
}
}
return cnt;
}
8.
有序数组中和为S的两个数字(Two Sum)
输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S。
vector<int> twoSum(vector<int> &numbers, int target)
{
vector<int> ret;
if(0 == numbers.size()) return ret;
map<int, int> minus2id;
for(size_t i = 0; i < numbers.size(); ++i)
{
if(minus2id.find(numbers[i]) != minus2id.end())
{
ret.push_back(1 + minus2id[numbers[i]]);
ret.push_back(1 + i);
} else {
minus2id[target - numbers[i]] = i;
}
}
return ret;
}
关于该题的扩展还有很多,3sum和4sum都是leetcode上的题目。
而需要求出任意个数字之和为S的情况,可以参见我的另一篇博文[算法]子数组之和问题
9.
加油站问题(leetcode中 Gas Station)
There are N gas stations along a circular route, where the amount of gas at station i is gas[i].
You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from station i to its next station (i+1). You begin the journey with an empty tank at one of the gas stations.
Return the starting gas station's index if you can travel around the circuit once, otherwise return -1.
Note:
The solution is guaranteed to be unique.
方法一:
初始值,start指向0,end指向n-1,如果油够用的话start依次往后扫描,否则end往前移动补充start位置不足的油量,直至前面的油够用。
int gasStation(vector<int> &gas, vector<int> &cost)
{
if(0 == gas.size()) return -1;
if(1 == gas.size())
{
if(gas[0] >= cost[0]) return 0;
else return -1;
}
int start = 0, end = gas.size() - 1;
int tank = 0;
while(start <= end)
{
tank += gas[start] - cost[start];
if(tank < 0)
{
while(start < end && tank < 0)
{
tank += gas[end] - cost[end];
--end;
}
if(tank < 0) return -1;
}
++start;
}
return start;
}
方法二:
从0至n-1扫描gas和cost数组,total统计每个位置的差值之和,用于判定是否有解。sum用来统计局部几个位置的差值之和,与上面思路不同的是,我们通过那里缺油来找终点。
int canCompleteCircuit(vector<int> &gas, vector<int> &cost)
{
int i = 0, end = -1;
int sum = 0, total = 0;
for(i = 0; i < gas.size(); ++i)
{
sum += (gas[i] - cost[i]);
total += (gas[i] - cost[i]);
if(sum < 0)
{
end = i;
sum = 0;
}
}
return total >= 0 ? end+1 : -1;
}
10.
求数组的“距离”,在给定数组a中找出这样的数对:i<j && a[i] < a[j],求最大的(j-i)
主要思路:构建min和max数组,min[i]表示数组a[0..i]的最小值,max[i]表示数组a[i..n-1]的最大值,这两个数组肯定都是非递增的,然后问题转变成从这两个有序数组中分别取一个数字x和y,使得x<y且
两者的
下标最小,可以用合并两个有序数组的思路。
int maxArrayDist(vector<int> &arr)
{
if(1 >= arr.size()) return -1;
int n = arr.size();
vector<int> minIdx, maxIdx;
minIdx.assign(n, 0);
maxIdx.assign(n, 0);
int tmp = arr[0];
for(int i = 0; i < n; ++i)
{
if(arr[i] < tmp) tmp = arr[i];
minIdx[i] = tmp;
}
tmp = arr[n-1];
for(int i = n-1; i >= 0; --i)
{
if(arr[i] > tmp) tmp = arr[i];
maxIdx[i] = tmp;
}
int ret = 0;
int i = 0, j = 0;
while(i < n && j < n)
{
if(maxIdx[j] > minIdx[i])
{
ret = max(ret, j - i);
++j;
}
else ++i;
}
if(0 == ret) return -1;
else return ret;
}