《剑指offer》数组专题

数组

记录《剑指offer》中所有关于数组的题目,以及LeetCode中的相似题目

相关题目列表

index description key words done date
3 二维数组查找 二维、查找 Y 18-1-16
8 旋转数组的最小数字 查找 Y 18-1-16
14 调整数组顺序使奇数在偶数之前 交换、分组 Y 18-1-18
20 顺时针打印矩阵 边界控制 Y 18-1-18
29 数组中出现次数超过一半的数字 top-k,partation Y 18-1-18
30 最小的k个数 top-k,堆 Y 18-1-18
31 连续子数组的最大和 动态规划 Y 18-1-19
33 把数组排成最小的数 与字符串的转化 Y 18-1-19
36 数组中的逆序对 类排序 Y 18-1-21
38 数字在排序数组中出现的次数 二分查找 Y 18-1-21
40 数组中只出现一次的数字 位运算应用 Y 18-1-23
51 数组中重复的数字 根据规律查找 Y 18-1-23
52 构建乘积数组 规律 Y 18-1-23
  • 说明 由于简书中的markdown不支持锚点,所以无法生成目录进行页内跳转,文章较长,如果需要阅读单一题目,只能ctrl+f 喽。

题目

数组是最简单的数据结构,其占据一块连续内存,在C++标准库STL中array表示数组,但是array在实际中应用的并不多,我们一般使用更全能的vector代替,可以简单讲array理解为vector<int>。

面试题3. 二维数组中的查找

题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样一个二维数组和一个整数,判断数组中是否含有该整数。

题目分析

如书中所给示例:
在下面矩阵中,查找数字5

矩阵示例

首先选取数组中右上角的数字<9>。如果该数字等于要查找的数字,查找过程结束;如果该数字大于要查找的数字,剔除这个数字所在的列<9,12,13,15>(因为这一列必定都大于所要查找的数字);如果该数字小于要查找的数字,剔除这个数字所在的行(因为这一行必定都大于要查找的数字)。

也就是说如果要查找的数字不在数组的右上角,则每一次都在数组的查找范围中剔除一行或者一列,这样每一步都可以缩小查找的范围,知道找到要查找的数字,或者查找范围为空。

参考代码

#include<iostream>
#include<vector>

using namespace std;

bool Find(int *matrix, int m, int n, int key)
{
    bool result = false;
    if (matrix != NULL && m > 0 && n > 0)
    {
        int row = 0;    //初始化所在行为第一行
        int column = n - 1;     //初始化所在列为最后一列,从而锁定右上角

        while (row < m && column > 0)
        {
            if (matrix[row * n + column] == key)    //matrix为右上角位置
            {
                result = true;
                break;
            }
            else if (matrix[row * n + column] > key)
                --column;       //所在列递减表明向左移动
            else
                ++row;  //所在行递增,表明逐渐向下移动
        }
    }
    return result;
}

//=====================测试样例=====================

void Test1()
{
    int m, n, key;
    int *matrix;

    cin >> m >> n >> key;
    //cout << endl;
    for (int i = 0; i < m * n; ++i)
    {
        cin >> matrix[i];
    }

    if (Find(matrix, m, n, key))
        cout << "true" << endl;
    else
        cout << "false" << endl;
}

int main()
{
    Test1();

    return 0;
}

上面的代码是通过指针来表示一个二维数组的,也可以通过vector<vector<int>> 来表示一个数组,用同样的方法完成题目。代码示例如下:

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if (matrix.empty())
            return false;
        bool res = false;
        int m = matrix.size();
        int n = matrix[0].size();

        int row = 0;
        int col = n - 1;
        while (row < m && col >= 0) {
            if (matrix[row][col] == target)
                return true;
            else if (matrix[row][col] > target) {
                --col;
            }
            else if (matrix[row][col] < target) {
                ++row;
            }
        }
        return res;
    }
};

相似题目

本题与LeetCode中的240. Search a 2D Matrix II完全一致,另外LeetCode中还有一道同为二维数组查找的题目74. Search a 2D Matrix
下面是这两道题的参考代码:
LeetCode 240 code
LeetCode 74 code

除LeetCode外,也可以在牛客网 剑指offer上完成本题。

面试题8. 旋转数组的最小数字

题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

题目分析

这道题最直观的做法就是直接遍历数组找出最小值,时间复杂度为O(n),但是这样就没有完全利用题目中旋转数组的条件。所以我们要想得到更高效的解决方法就需要在旋转数组中找到突破口。

参考代码

#include<iostream>

using namespace std;

//遍历数组,得到最小值
int MinInOrder(int *numbers, int index1, int index2)
{
    int result = numbers[index1];
    for (int i = index1 + 1; i <= index2; ++i)
    {
        if (numbers[i] < result)
            result = numbers[i];
    }

    return result;
}

int Min(int *numbers, int length)
{
    int index1 = 0;
    int index2 = length - 1;
    int MidIndex = index1;  //当不满足while循环条件时,证明数组本身有序,直接返回第一个元素,即是最小元素

    if (numbers == NULL || length <= 0)
        return -1;

    while (numbers[index1] >= numbers[index2])    //一般情况下旋转数组的特性
    {
        if (index1 == index2 - 1)    //循环中止条件
        {
            MidIndex = index2;
            break;
        }

        MidIndex = (index1 + index2)/2;

        //如果三个指针位置数值大小相等,则无法确定中间位置属于递增部分还是递减部分,只能采用遍历方式
        if (numbers[index1] == numbers[MidIndex] && numbers[index1] == numbers[index2])
            return MinInOrder(numbers, index1, index2);


        if (numbers[MidIndex] >= numbers[index1])
            index1 = MidIndex;
        else if (numbers[MidIndex] <= numbers[index2])
            index2 = MidIndex;
    }

    return numbers[MidIndex];
}

void test1()    //一般测试样例
{
    int numbers[] = {3,4,5,1,2};
    cout << Min(numbers, 5) << endl;
}

void test2()    //测试样例2,测试全部相等的数
{
    int numbers[] = {1,1,1,1,1};
    cout << Min(numbers, 5) << endl;
}

void test3()    //测试样例3,测试index1, index2, MidIndex 相等
{
    int numbers[] = {1,0,1,1,1};
    cout << Min(numbers, 5) << endl;
}

void test4()
{
    int numbers[] = {1,2,3,4,5};
    cout << Min(numbers, 5);
}

int main()
{
    test1();
    test2();
    test3();
    test4();

    return 0;
}

参考代码中是以指针的形式表示数组,同样也可以以vector<int>的方式。
因为代码中都带有详细的注释,所以如果不是特别复杂的逻辑结构,都直接以代码的形式展示题解。Just Show Me Your Code!

相似题目

本题与LeetCode中的153. Find Minimum in Rotated Sorted Array完全一致。可以在上面进行代码验证。
另外,LeetCode中还有本题中关于数组旋转的题目189. Rotate Array
下面是LeetCode中两道题的参考代码:
LeetCode 153 code
LeetCode 189 code

除LeetCode外也可以在牛客网 剑指offer中完成本题。

题目14. 调整数组顺序使奇数位于偶数之前

题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

题目分析

数组示例

对于这道题,很容易想到的方法是维护两个指针leftright,分别指向数组的首尾位置,采用双向遍历的方式,如果left指向的元素为偶数,且right指向的元素为奇数,则交换两个元素。直到遍历玩整个数组,即可满足条件。参考代码如1。

值得注意的是,对于这道题,存在扩展性的解法,因为题目要求是区分奇偶数,这只是这类问题的一个特例,我们也可以通过传入函数的指针形式,完成这类问题的扩展性解法。参考代码如2。

参考代码

cpp1

#include<iostream>
#include<vector>

using namespace std;

class Solution
{
public:
    void ReorderArray(vector<int>& nums)
    {
        int start = 0;
        int end = nums.size() - 1;

        while (start < end)
        {
            while (nums[start] % 2 == 1)
                start++;
            while (nums[end] % 2 == 0)
                end--;

            if (start < end)
                Swap(&nums[start], &nums[end]);
        }
    }
private:
    void Swap(int* i, int* j)
    {
        int temp;
        temp = *i;
        *i = *j;
        *j = temp;
    }
};

cpp2

/*=============将函数写成模式=================*/
void Swap(int* i, int* j)
{
    int temp = *i;
    *i = *j;
    *j = temp;
}

void Recorder(vector<int>& nums, bool (*func)(int))
{
    if(nums.empty())
        return;

    int start = 0;
    int end = nums.size() - 1;
    while (start < end)
    {
        while (!func(nums[start]))
            start++;
        while (func(nums[end]))
            end--;

        if (start < end)
            Swap(&nums[start], &nums[end]);
    }
}

bool isEven(int n)
{
    return (n % 2 == 0);
}

void RecorderOddEven(vector<int> &nums)
{
    Recorder(nums, isEven);
}

相似题目

可以在牛客网 剑指offer上完成对本题的验证。

面试题20: 顺时针打印矩阵

题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。例如,如果输入如下矩阵:

矩阵示例

则依次打印出数字1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16。

题目分析

这道题并没有复杂的数据结构和算法分析,主要考察的是对边界条件的控制,这需要通过实例慢慢验证。
打印第一圈的左上角的坐标是(0,0),第二圈的左上角的坐标是(1,1),依次类推。我们注意到,左上角的坐标中行和列总是相同的,可以在矩阵中选取左上角为(start,start)的一圈作为我们分析的目标。
这道题没有涉及复杂的数据结构和高级的算法,看起来是一个简单的问题,但是要解决这个问题,会在代码中包含多个循环,并且还需要判断多个边界条件。
由于是以外圈到内圈的顺序依次打印,我们可以把矩阵想象成若干个圈。利用每次循环打印一个圈。
对于一个5×5的矩阵而言,最后一圈只有一个数字,对应的坐标是(2,2),我们发现5>2×2。对于一个6×6的矩阵而言,最后一圈有4个数字,其左上角的坐标仍然是2×2。同样6>2×2.于是我们可以得到循环继续的条件时columns>startX × 2并且rows> startY× 2。
接下来我们考虑如何实现打印一圈的功能。我们可以把打印一圈分为四步:第一步从左到右打印一行,第二步从上到下打印一列,第三步从右到左打印一行,第四部从下到上打印一列。
仔细分析打印时每一步的前提条件。第一步总是必须的。第二步的前提条件是终止行号大于起始行号。第三步的打印条件是起始列号小于终止列号,并且第二步条件也需要满足。第四步的前提条件是终止行号比起始行号至少大2,同时终止列号大于起始列号。

参考代码

#include<iostream>
#include<vector>

using namespace std;

class Solution {
public:
    void printMatrix(vector<vector<int> > matrix) {
        int rows = matrix.size();
        int columns = matrix[0].size();
        if (rows <= 0 && columns <= 0)
            return;

        int start = 0;
        while (rows > start * 2 && columns > start * 2){
            PrintACircle(matrix, start, rows, columns);
            start++;
        }
    }
private:
        void PrintACircle(vector<vector<int>> matrix, int start, int rows, int columns){
            int endX = rows - 1 - start;
            int endY = columns - 1 - start;

            for (int i = start; i <= endY; ++i){
                cout << matrix[start][i] << ",";
            }
            if (endX > start){
                for (int i = start + 1; i <= endX; ++i){
                    cout << matrix[i][endY] << ",";
                }
            }
            if (endX > start && endY > start){
                for (int i = endY - 1; i >= start; --i){
                    cout << matrix[endX][i] << ",";
                }
            }
            if (endX > start + 1 && endY > start){
                for (int i = endX - 1; i > start; --i){
                    cout << matrix[i][start] << ",";
                }
            }
        }
};

int main()
{
    vector<vector<int>> nums = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12}, {13,14,15,16}};
    Solution solu;
    solu.printMatrix(nums);

    return 0;
}

相似题目

本题与LeetCode中的54. Spiral Matrix完全一致,可以在上面进行验证。
此题参考代码见:
LeetCode 54 code     这里与参考代码是不同的,以方便选择自己认为可读性更好的代码进行阅读。
还可以在牛客网 剑指offer上对编码进行验证。

面试题29: 数组中出现次数超过一半的数字

题目: 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

题目分析

本题最容易想到的方法就是排序,排序数组的中间位置即为所求。
接下来思考是否还有效率更高的方法。由题目可知,该题就是找到数组中的中位数。即长度为n的数组中第n/2大的数字。这属于一类问题,统称为Top-k问题。我们有成熟的算法完成此类问题。

参考代码

基于partation的方法

class Solution {
public:
    /*
    //完全排序方式
    int majorityElement(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int middle = nums.size()/2;
        return nums[middle];
    }
    */

    //partation做法
    int majorityElement(vector<int>& nums) {
        int length = nums.size();
        int middle = length >> 1;
        int start = 0;
        int end = length - 1;
        int index = Partation(nums, start, end);

        //直到找到middle
        while (index != middle){
            if (index > middle){
                end = index - 1;
                index = Partation(nums, start, end);
            }
            else if (index < middle){
                start = index + 1;
                index = Partation(nums, start, end);
            }
        }
        return nums[middle];
    }
private:
    int Partation(vector<int>& nums, int start, int end){
        int small = start - 1;
        //int index = start;
        int temp = Random(start, end);
        //交换标准
        Swap(&nums[temp], &nums[end]);
        for (int i = start; i < end; ++i){
            if (nums[i] < nums[end]){
                ++small;
                if (small != i)
                    Swap(&nums[small], &nums[i]);
            }
        }
        ++small;
        Swap(&nums[small], &nums[end]);

        return small;
    }

    int Random(int start, int end){
        if (end > start)
            return start + rand() % (end - start);
        else
            return 0;
    }

    void Swap(int* i, int* j){
        int temp = *i;
        *i = *j;
        *j = temp;
    }
};

还可以采用堆或者红黑树的方法完成Top-k问题,这里在第30题中会介绍。

另外根据本题的n/2的特征,还有一种适应于本题做法: 数组存在一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数和还多。因此,我们可以在遍历数组的过程中维护两个值:一个是数组的元素值,一个是次数。当我们遍历到下一个数字的时候,如果下一个数字和之前保存的数字相同,则次数加1;如果不同,则次数减1.如果次数为0,则保存下一个元素,并把次数设为1。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还多,那么要找的数字肯定就是最后一次把次数设为1时对应的数字,并且这种解法不需要改变数组本身,也不需要额外空间。

class Solution2{
public:
    int MoreThanHalfNum(vector<int> numbers){
        if (numbers.size() == 0)
            return 0;
        int result = numbers[0];
        int times = 1;
        for (int i = 1; i < numbers.size(); ++i){
            if (times == 0){
                result = numbers[i];
                times = 1;
            }
            else if (numbers[i] == result)
                times++;
            else
                times--;
        }
        return res;
    }
}

相似题目

本题与LeetCode中的169. Majority Element完全一致,可以在上面进行代码验证。
此题代码可以参考:Leetcode 169 code
另外也可以在牛客网 剑指offer上进行代码验证。

面试题30: 最小的k个数

题目: 输入n个整数,找出其中最小的k个数。例如,输入4,5,1,6,2,7,3,8 这8个数字,则最小的4个数字是1,2,3,4。

题目分析

虽然这不是一道明确的数组问题,但是因为其与29题同属于top-k问题,所以也将其放在数组专题中。这里介绍top-k问题的堆排序解法,这种解法适合海量数据问题。

最大堆中根结点的值总是大于它的子树中任意节点的值。于是我们每次可以在O(1)得到已有的k个数字中的最大值。

参考代码中给出了partation的做法,利用最大堆的做法,以及利用红黑树的做法。主要建议采用最大堆。

参考代码

#include<iostream>
#include<vector>
#include<stdlib.h>
using namespace std;

/*==============常规数据===================*/
class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> result;
        if (k > input.size() || k <= 0)
            return result;

        int start = 0;
        int end = input.size() - 1;
        int index = Partation(input, start, end);
        while (index != k - 1){     //找到所在位置的数为止
            if (index > k - 1){
                end = index - 1;
                index = Partation(input, start, end);
            }
            if (index < k - 1){
                start = index + 1;
                index = Partation(input, start, end);
            }
        }

        for (int i = 0; i < k; ++i){
            result.push_back(input[i]);
        }
        return result;
    }

private:
    void Swap(int* i, int* j){
        int temp = *i;
        *i = *j;
        *j = temp;
    }

    int Random(int start, int end){
        if (end > start){
            return start + rand() % (end - start);
        }
        else
            return 0;
    }

    int Partation(vector<int>& data, int start, int end){       //因为要改变data所以采用引用
        int middle = Random(start, end);
        Swap(&data[middle], &data[end]);    //以end处作为基准

        int small = start - 1;      //哨兵作用
        for (int i = start; i < end; ++i){
            if (data[i] < data[end]){
                ++small;
                if (small != i)     //防止多余交换
                    Swap(&data[small], &data[i]);
            }
        }

        ++small;
        Swap(&data[small], &data[end]);

        return small;
    }
};

/*==================海量数据采用最大堆================*/
/*
class Solution{
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k){
        int len = input.size();
        if (len <= 0 || k > len || k <= 0)
            return vector<int>();       //输出空vector
        vector<int> res(input.begin(), input.begin() + k);  //结果中装填前k个数
        //建立最大堆
        make_heap(res.begin(), res.end());
        for (int i = k; i < len; ++i){
            if (input[i] < res[0]){
                //先pop,然后再容器中删除
                pop_heap(res.begin(), res.end());   //将最大元素pop并将剩余元素重新维护为一个堆
                res.pop_back();
                //先在容器中加入,再push
                res.push_back(input[i]);
                push_heap(res.begin(), res.end());     //对刚插入的元素做堆排序
            }
        }
        sort_heap(res.begin(), res.end());  //从小到大
        return res;
    }
};
*/

/*==================海量数据利用红黑树_multiset=================*/
/*
class Solution{
public:
    vector<int> GetleastNumbers_Solution(vector<int> input, int k){
        int len = input.size();
        if (len <= 0 || k > len || k <= 0)
            return vector<int>();
        //仿函数中的greater<T>模板,从大到小排序
        multiset<int, greater<int>> leastNums;
        vector<int>::iterator vec = input.begin();
        for (; vec != input.end(); ++vec){
            //将前k个元素插入集合
            if (leastNums.size() < k)
                leastNums.insert(*vec);
            else{
                //第一个元素是最大值
                //multiset<int, greater<int>>::iterator greatest = leastNums.begin();
                //如果后续元素小于最大值,删除第一个,插入当前元素
                if (*vec < *(leastNums.begin()){
                    leastNums.erase(leastNums.begin());
                    leastNums.insert(*vec);
                }
            }
        }
        return vector<int>(leastNums.begin(), leastNums.end());
    }
};
*/


int main()
{
    vector<int> input = {4,5,1,6,2,7,3,8};
    Solution solu;
    vector<int> output = solu.GetLeastNumbers_Solution(input, 4);

    for (int i = 0; i < 4; ++i){
        cout << output[i] << " ";
    }

    return 0;
}

相似题目

此题与LeetCode中的215. Kth Largest Element in an Array相似,LeetCode中是求最大的k个数,因此采用最小堆。
可以参考LeetCode 215 code
同样也可以在牛客网 剑指offer中对代码进行验证。

面试题31: 连续子数组的最大和

题目:输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如输入的数组为{1,2,3,10,-4,7,2,-5},和最大的子数组为{3,10,-4,7,2},因此输出的子数组和为18。

题目分析

这是一道简单的动态规划的题,只需维护一个局部变量和一个全局变量即可通过遍历一遍数组完成题解。可以直接通过代码理解。

参考代码

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {

        int local_max = array[0];
        int global_max = array[0];
        for (int i = 1; i < array.size(); ++i){
            local_max = local_max + array[i];
            local_max = max(local_max, array[i]);
            global_max = max(local_max, global_max);
        }
        return global_max;
    }
};

相似题目

本题与LeetCode中的53. Maximum Subarray完全一致,另外LeetCode中还有一道子数组最大积的问题152. Maximum Product Subarray,同样也可以利用本题中的思想完成。
下面是这两道题的参考代码:
LeetCode 53 code
LeetCode 152 code

除LeetCode外,也可以在牛客网 剑指offer上完成本题。

面试题33: 把数组排成最小的数

题目: 输入一个正整数数组,把数组里所有数字拼接起来构成一个数,打印能拼接处的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这3个数字能排成的最小数字321323。

题目分析

这道题最直接的做法是先求出数组中所有数字的全排列,然后得出最小值。但是这样n个数字会有n!个排列。效率会很差,我们应该从排序规则入手,找到更高效的解题方法。
要确定一个排序规则,则要比较两个数字,对于m和n有两种排序方法mn、nm,我们需要判断的是mn与nm的大小。而如何比较一个拼接数字的大小,最简单的方法就是将数字转化为字符串。参考代码如下。

参考代码

#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
class Solution {
public:
    static bool compare(int a, int b){  //传入sort中的参数必须为static
        string A = "";
        string B = "";
        A += to_string(a);
        A += to_string(b);
        B += to_string(b);
        B += to_string(a);

        return A < B;   //这里的小于号是字符串的比较
    }
    string PrintMinNumber(vector<int> numbers) {
        string res = "";
        sort(numbers.begin(), numbers.end(), compare);
        for (int i = 0; i < numbers.size(); ++i){
            res += to_string(numbers[i]);
        }

        return res;
    }
};

int main()
{
    vector<int> nums = {3,32,321};
    Solution solu;
    cout << solu.PrintMinNumber(nums) << endl;

    return 0;
}

相似题目

本题与LeetCode中的179. Largest Number题类似,只是LeetCode中要求的是排列成最大的数。
此题的参考代码可见:
LeetCode 179 code

可以在牛客网 剑指offer中验证代码的正确性。

面试题36: 数组中的逆序对

题目: 在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数组组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
例如,在数组{7,5,6,4}中,一共存在5个逆序对,分别是(7,6),(7,5),(7,4),(6,4),(5,4)。

题目分析

以数组{7,5,6,4}为例分析统计逆序对的过程。我们考虑先比较相邻的数字。
如图1与图2所示,先把数组分解为两个长度为2的子数组,再把这两个子数组分别拆分成两个长度为1的子数组。接下来一边合并相邻的子数组,一边统计逆序对的数目。

图1

图2

对算法熟悉的人可以发现图1和图2所描述的过程正是归并排序。先把数组分割成子数组,先统计出子数组内部的逆序对数目,然后再统计出两个相邻子数组之间的逆序对数目。在统计逆序对的过程中,还需要对数组进行排序。同理,冒泡排序也能完成本题,但是冒泡排序其实就是本题的暴力方法。

参考代码

#include<iostream>
#include<vector>
using namespace std;

class Solution {
public:
    int InversePairs(vector<int> data) {
        int length = data.size();
        if (length <= 0)
            return 0;
        vector<int> copy(length);

        int count = InversePairsCore(data, copy, 0 ,length - 1);
        return count;
    }
private:
    int InversePairsCore(vector<int> &data, vector<int> &copy, int start, int end){
        if (start == end){      //递归结束条件,当只有一个数时返回
            copy[start] = data[start];      //将最后数据存入copy中
            return 0;
        }

        int length = (end - start) / 2;

        int left = InversePairsCore(data, copy, start, start + length);
        int right = InversePairsCore(data, copy, start + length + 1, end);

        //i初始化为前半段最后一个
        int i = start + length;
        //j初始化为后半段最后一个
        int j = end;
        int indexCopy = end;
        int count = 0;
        while (i >= start && j >= start + length + 1){
            if (data[i] > data[j]){
                copy[indexCopy] = data[i];
                indexCopy--;
                i--;
                count += (j - start - length);
            }
            else{
                copy[indexCopy] = data[j];
                indexCopy--;
                j--;
            }
        }

        for (; i >= start; --i){
            copy[indexCopy] = data[i];
            indexCopy--;

        }
        for (; j >= start + length + 1; --j){
            copy[indexCopy] = data[j];
            indexCopy--;
        }

        for (int i = start; i <= end; ++i){     //保证data有序
            data[i] = copy[i];
        }
        return count + left + right;

    }
};

int main()
{
    vector<int> data = {7,5,6,4};
    Solution solu;
    cout << solu.InversePairs(data) << endl;

    return 0;
}

相似题目

可以在牛客网 剑指offer上完成本题。

面试题38: 数字在排序数组中出现的次数

题目: 统计一个数字在排序数组中出现的次数。例如输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4。

题目分析

本题是一个统计次数的问题,可以直接一遍便利即可得出结果,这样的时间复杂度为O(n)。但是这样没有利用到数组的排序特性。这里可以使用两次二分查找,以题目中的例子说明,只要找到第一个3与最后一个3的位置,即可得出题解。

参考代码

#include<iostream>
#include<vector>

using namespace std;

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        if (data.size() <= 0)
            return 0;
        int FirstK = GetFirstK(data, k, 0, data.size() - 1);
        int LastK = GetLastK(data, k, 0, data.size() - 1);

        if (LastK != -1 && FirstK != -1)
            return LastK - FirstK + 1;
        else
            return 0;

    }
private:
    int GetFirstK(vector<int> data, int k, int start, int end){
        if (start > end)    //递归结束条件
            return -1;
        int middle = start + (end - start) / 2;
        if (data[middle] == k){
            if ((middle > 0 && data[middle - 1] != k) || middle == 0)
                return middle;
            else
                end = middle - 1;
        }
        else if (data[middle] > k){
            end = middle - 1;
        }
        else if (data[middle] < k){
            start = middle + 1;
        }
        return GetFirstK(data, k, start, end);
    }

    int GetLastK(vector<int> data, int k, int start, int end){
        if (start > end)    //递归结束条件
            return -1;
        int middle = start + (end - start) / 2;
        if (data[middle] == k){
            if ((middle < end && data[middle + 1] != k) || middle == end)
                return middle;
            else
                start = middle + 1;
        }
        else if (data[middle] > k){
            end = middle - 1;
        }
        else if (data[middle] < k){
            start = middle + 1;
        }
        return GetLastK(data, k, start, end);
    }
};

int main()
{
    Solution solu;
    vector<int> data = {1,3,3,3,3,4,5};
    cout << solu.GetNumberOfK(data,2) << endl;

    return 0;
}

相似题目

可以在牛客网 剑指offer中完成对本题的练习。

面试题40: 数组中只出现一次的数字

题目: 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度为O(n),空间复杂度为O(1)。

题目分析

看到此题最直接的想法就是构建一个hash table,但是这样的空间复杂度不符合题目要求。所以要根据题目中的除了两个数字之外其他的都出现了两次来找到更高效的解法。

我们先假设数组中只有一个数字出现了一次,其他数字都出现了两次。根据异或运算的性质:任何一个数字异或它自己都等于0.也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终得到的就是那个只出现一次的数字。

回到原始题目,看看能否采用相同的思想。如果我们能够将原数组分成两个数组,而两个数字正好分别在这两个数组中就好了。

首先,我们从头到尾依次异或数组中的每一个数字,最终的到的是两个目标数字的异或结果。我们可以根据这个结果对数组进行分组。

由于这两个数字肯定是不一样的,所以最终的异或结果肯定不为0,也就是说这个结果中至少有一位为1(二进制)。将第一个1的位置即为n,则根据第n位是否为1给数组分组,则相同数组肯定会被分到一组,而n位是由两个目标数字异或得到的,所以必定在此为上一个为0一个为1。所以可以正确的将数组分为两组,并且两个目标数字分别处于两组中。

可以以《剑指offer》中{2,4,3,6,3,2,5,5}为例进行上面的步骤理解。

参考代码

#include<iostream>
#include<vector>

using namespace std;

/*=============利用vector构建hash==================*/
class Solution
{
public:
    vector<int> FindNumsAppearOnce(vector<int> data)
    {
        vector<int> result;
        vector<int> hashtable(data.size(), 0);
        for (int i = 0; i < data.size(); ++i)
        {
            hashtable[data[i]]++;
        }
        for (int i = 0; i < hashtable.size(); ++i)
        {
            if (hashtable[data[i]] == 1)
               result.push_back(data[i]);
        }
        return result;
    }
};

/*================时间复杂度为O(n),空间复杂度为O(1)的方法=================*/
class Solution2
{
public:
    void FindNumsAppearOnce(vector<int> data, int* num1, int* num2)
    {
        if (data.size() <= 0)
            return;
        int result_temp = 0;
        for (int i = 0; i < data.size(); ++i)
        {
            result_temp = result_temp ^ data[i];
        }
        int TheFirst1 = FindFirst1(result_temp);
        *num1 = *num2 = 0;
        for (int i = 0; i < data.size(); ++i)
        {
            if (Is1(data[i], TheFirst1))
                *num1 = *num1 ^ data[i];
            else
                *num2 = *num2 ^ data[i];
        }
    }
private:
    int FindFirst1(int num)
    {
        int index = 0;
        while (((num & 1) == 0) && index < 8*sizeof(int))
        {
            num = num >> 1;
            index++;
        }
        return index;
    }

    bool Is1(int num, int index)
    {
        num = num >> index;
        return (num & 1);
    }

};

int main()
{
    Solution solu;
    vector<int> result;
    vector<int> data = {2,4,3,6,3,2,5,5};
    result = solu.FindNumsAppearOnce(data);

    for (int i = 0; i < result.size(); ++i)
    {
        cout << result[i] << " ";
    }

    return 0;
}

相似题目

本题与LeetCode中的260. Single Number III完全一致,另外,LeetCode中还有一道此题的简化版本,即找到唯一的一个出现次数为1的数字136. Single Number;
LeetCode中还有一道此题的扩展版本,除了一个数字出现1次其他出现3次,要求找出这个唯一的数字137. Single Number II
这三道题的参考代码见:
LeetCode 260 code
LeetCode 136 code
LeetCode 137 code

还可以在牛客网 剑指offer中完成对本题的练习。

面试题51: 数组中重复的数字

题目: 在一个长度为n的数组中的所有数字都在0~n-1之间。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中第一个重复的数字。

题目分析

看到此题最直接的想法是将数组排序,在排序数组中找到重复元素是很容易的一件事。
当然还可以利用hash的方法来解决这个问题。

但是都没有充分利用到0~n-1这个条件,下面我们看有没有更高效的方法。

由于数组中的元素都在0~n-1的范围内,如果这个数组中没有重复的数字,那么当数组排序之后数字i将出现在下标为i的位置。由于数组中存在重复的数字,则有些位置可能存在多个数字,有些位置可能没有数组。根据这个特点我们得到下面的解法。

从头到尾依次扫描数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字(用m表示)是否等于i。如果是,接着扫描下一个数字。如果不是,则那它与第m个数字进行比较。如果它和m个数字相等,就找到了第一个重复数字,如果不等,则把第i个数字与第m个数字交换位置,让m回到属于它的下标位置去。接下来重复这个比较,交换的过程,即可找出所有重复数字。

可以以书中给出的{2,3,1,0,2,5,3}数组为例进行上面的分析理解。

参考代码

#include<iostream>
#include<vector>

using namespace std;

class Solution {
public:
    //暴力解法
    bool duplicate(vector<int> numbers, int length, int* duplication) {
        if (length <= 0)
            return false;

        bool found = false;
        for (int i = 0; i < length; ++i){
            for (int j = i + 1; j < length; ++j){
                if (numbers[i] == numbers[j]){
                    *duplication = numbers[i];
                    found = true;
                    break;
                }
                if (found == true)
                    break;
            }
        }
        return found;
    }

    //答案解法
        bool duplicate2(vector<int> numbers, int length, int* duplication) {
        if (length <= 0)
            return false;
        for (int i = 0; i < length; ++i){
            if (numbers[i] > length -1 || numbers[i] < 0)
                return false;
        }

        for (int i = 0; i < length; ++i){
            while (numbers[i] != i){
                if (numbers[i] == numbers[numbers[i]]){
                    *duplication = numbers[i];
                    return true;
                }
                Swap(&numbers[i],&numbers[numbers[i]]);
            }
        }
        return false;
    }
    //还可以利用hash,先排序等解法。
private:
    void Swap (int* i, int* j)
    {
        int temp = *i;
        *i = *j;
        *j = temp;
    }

};

int main()
{
    vector<int> numbers = {2,3,1,0,2,5,3};
    Solution solu;
    int duplication1 = 0;
    int duplication2 = 0;

    bool result1 = solu.duplicate(numbers, numbers.size(), &duplication1);
    bool result2 = solu.duplicate2(numbers, numbers.size(), &duplication2);

    cout << result1 << " " << duplication1 << endl;
    cout << result2 << " " << duplication2 << endl;

    return 0;

}

相似题目

此题与LeetCode中的287. Find the Duplicate Number完全一致。此题的参考代码见:
LeetCode 287 code
还可以在牛客网 剑指offer上完成对本题的练习。

面试题52: 构建乘积数组

题目: 给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]×A[1]×...×A[i-1]×A[i+1]×...×A[n-1]。不能使用除法。

题目分析

本题要求不能使用除法,直观的解法是直接连乘n-1个数字得到B[i],但是显然这种做法并不是我们想得到的结果。

此题更高效的方法是将B[i]=A[0]×A[1]×...×A[i-1]×A[i+1]×...×A[n-1]分为两部分解决,从i处分割为A[0]×A[1]×...×A[i-1]与A[i+1]×...×A[n-1]。

不妨设C[i]=A[0]×A[1]×...×A[i-1];D[i]=A[i+1]×...×A[n-1],则C[i]=C[i-1]×A[i-1]; D[i]=D[i+1]×A[i+1];

参考代码

#include<iostream>
#include<vector>
using namespace std;

class Solution {
public:
    //将数组分为两部分
    vector<int> multiply1(const vector<int>& A) {
        int length = A.size();
        vector<int> result(length);
        if (length <= 0)
            return result;

        //赋值前半部分
        result[0] = 1;
        for (int i = 1; i < length; ++i){
            result[i] = result[i - 1] * A[i - 1];
        }

        //赋值后半部分
        int temp = 1;
        for (int i = length - 1; i >= 0; --i){
            result[i] = result[i] * temp;
            temp = temp * A[i];
        }
        return result;
    }

    //同样是分为两组,可读性更好的代码
    vector<int> multiply(const vector<int>& A) {
        int count = A.size();
        vector<int> res(count, 1);
        vector<int> left(count, 1);
        vector<int> right(count, 1);

        for (int i = 1; i < count; ++i){
            left[i] = left[i-1] * A[i-1];
        }
        for (int i = count - 2; i >= 0; --i){
            right[i] = right[i+1] * A[i+1];
        }

        for (int i = 0; i < count; ++i){
            res[i] = left[i] * right[i];
        }
        return res;
    }
};

int main()
{
    vector<int> A = {1,2,3,4,5};
    vector<int> result;
    Solution solu;
    result = solu.multiply(A);

    for (int i = 0; i < result.size(); ++i)
    {
        cout << result[i] << " " << endl;
    }

    return 0;
}

相似题目

本题与LeetCode中的238. Product of Array Except Self完全一致,代码见:
LeetCode 238 code
同样还可以在牛客网 剑指offer上完成对本题的练习。

【参考】
[1]《剑指offer》

欢迎转载,转载请注明出处:wenmingxing 《剑指offer》数组专题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342