关于数组的面试题总结(二)

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;
}



你可能感兴趣的:(关于数组的面试题总结(二))