编程面试_数组

数组面试题

    笔试和面试中,除了字符串,另一类出现频率极高的问题便是与数组相关的问题。
    解题思路:
    先一般考虑“万能的”暴力穷举(递归、回溯),如求n个数的全排列或八皇后(N皇后问题)。
    但因为穷举时间复杂度通常过高,所以需要考虑更好的方法。

如 分治法(通过分而治之,然后归并);

以及 空间换时间(如活用哈希表)。

此外,选择合适的 数据结构 可以显著提升效率,如寻找最小的k个数中,用 堆 代替 数组,也可借鉴快排思想,中枢分区方法。

再有,如果题目允许排序,则可以考虑 排序。

寻找和为定值的两个数中,先排序,然后用 前后两个指针 往中间扫描。

    而如果如果已经排好序了(如杨氏矩阵查找中),则想想有无必要二分,二分法。

    但是,如果题目不允许排序呢?这个时候,

我们可以考虑 不改变数列顺序的 贪心算法(如 最小生成树Prim、Kruskal 及 最短路dijkstra)

    或 动态规划(如 01背包问题,每一步都在决策)。

    最后,注意细节处理,不要忽略边界条件,如字符串转换成整数。

基础

int (*p)[10];// 首先(*p),p是一个指针,int [10],是一个包含10个int的数组,该指针p指向,这个数组
             // 是数组指针
int *p[10];  // 写成 int* p[10];比较合适,首先 [10]是一个包含 10个元素的数组,该元素类型为int*,即指向整形的指针
             // 是指针数组

int a[10];// 整数数组
printf("%x\n",&a);// &a 是取一个数组的地址,是一个指针指向一个包含10个元素的数组,类型为 int (*p)[10];
printf("%x\n",a); // a 数组名,默认为数组首元素 a[0]的地址 和数组a的地址是同一个地址,该类型为指向一个整数的指针,即 int *p;
// 以上 &a 和 a 的值是一样的,是a[0]元素的存储地址,
// 但是含义不一样, &a指向了10个连续存储的整数, 而 a指向了一个 整数,即 a[0]

// 类似的

int (*p[])(int); // 函数指针数组。
// 首先(*p[]) 是一个指针数组,指向的类型为 int (int)是一个函数类型,输入一个int类型参数,输出一个int类型参数

int (*p())[];    // 返回数组指针的函数。
// 首先 (*p()) 是一个返回指针类型的函数,该指针类型指向 int [] 即整形数组,即,p是一个返回整形数组指针的函数

int *p()[]       // 字面上可以解释为返回指针数组的函数,不过函数是不能返回数组的。
// () 是一个函数, 返回类型为 int* [],是一个指针数组

int *(*a())()    // 这是一个函数,它没有参数,它的返回值是一个函数指针,这个指针指向的函数,也没有参数,且返回值是int型的指针。

第一题 寻找集合中的最小的K个数

输入n个整数,输出其中最小的k个。

解法1 先排序再选择

    要求一个序列中最小的k个数,按照惯有的思维方式,
    则是先对这个序列从小到大排序,
    然后输出前面的最小的k个数。
    至于选取什么的排序方法,我想你可能会第一时间想到快速排序
    我们知道,快速排序平均所费时间为n*logn,
    然后再遍历序列中前k个元素输出即可。

因此,总的时间复杂度:O(n * log n)+O(k)=O(n * log n)。

解法2 选择排序思想

解法二

    咱们再进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。
    既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序,即:
    
    1、遍历n个数,把最先遍历到的k个数存入到大小为k的数组中,假设它们即是最小的k个数;
    
    2、对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为O(k));
    
    3、继续遍历剩余n-k个数。
       假设每一次遍历到的新的元素的值为x,把x与kmax比较:如果x < kmax ,用x替换kmax,

并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果x >= kmax,则继续遍历不更新数组,直到,k个数是此时最小的
         
每次遍历,更新或不更新数组的所用的时间为O(k)或O(0)。故整趟下来,时间复杂度为nO(k)=O(nk)。

解法3 利用最大堆 作为容器

    更好的办法是维护容量为k的最大堆,原理跟解法二的方法相似:
    
    1、用容量为k的最大堆存储最先遍历到的k个数,同样假设它们即是最小的k个数;
    
    2、堆中元素是有序的,令k1<k2<...<kmax(kmax设为最大堆中的最大元素)
    
    3、遍历剩余n-k个数。
      假设每一次遍历到的新的元素的值为x,把x与堆顶元素kmax比较:
      如果x < kmax,用x替换kmax,然后更新堆(用时logk);
      否则不更新堆。
    这样下来,总的时间复杂度:O(k+(n-k)*logk)=O(n*logk)。
    此方法得益于堆中进行查找和更新的时间复杂度均为:O(logk)
    (若使用解法二:在数组中找出最大元素,时间复杂度:O(k))。  

解法4 利用快排思想 得到大小数的 分区中枢

    int Cut(int a[], int low, int high){
      int temp = a[low];// 区间第一个元素 可以随机选择 在替换到low的位置
      while(low < high){
        while((low < higt)&&(a[high] > temp)) --high;// 从右边向左寻找比 temp小的元素 a[higt]
        a[low] = a[high];// 把比temp小的元素 放在 原来temp的位置(左边小的区域)
        while((low < high)&&(a[low] < temp))  ++low;// 从左边寻找比 temp大的元素a[low] 放在右边 high的位置
        a[high] = a[low];
       }
      a[low] = temp; // 临时变量 放在 中枢位置
      return low;    // 返回中枢位置
    }

    // 在数组a中选择k个最小的元素  块选  非递归调用
    void QuickSelest(int a[], int low, int high, int k){
      int index =  Cut(a, 0, n-1);// 首先得到一个中枢
      while(index != k-1){// 非递归调用
      if(index > k - 1) index = Cut(a, low, index -1);// 左半部分小的元素个数大于k 在左半部分找 中枢
      else              index = Cut(a, index+1, high);// 左半部分小的元素个数小于k 右半部分还有,在右半部分找 中枢
      }
     // 打印
     for(int i = 0; i<k; ++i) cout<<a[i];
     cout << endl;
    }

    // 而快排为   是递归调用
    void QuickSort(int a[], int low, int high){
      int index;
      if(low < high){
        index = Cut(a, low, high);//选取中枢位置
        QuickSort(a, low, index-1);//对左边快排
        QuickSort(a, index+1, high);//对右边快排
      }
    }

    // 二分查找   非递归版本
    int BinarySearch(int a[], int low, int high, int key){
    int mid;//中间元素
    while(low < high){
    int mid = low + ((high - low)>>1);//中间元素的 下标 在循环体内 不停的被改变
    if( key < a[mid] )     high = mid - 1;// 查找的元素小于中值  高区间移至 mid-1
    elae if(key > a[mid] ) low  = mid + 1;// 查找元素大于中值, 将低区间移至 mid+1
    else return mid;
    }
    return -1;//未找打 返回-1
    }

    // 二分查找   递归版本
    int BinarySearch(int a[], int low, int higt, int key){
    int mid = low + ((high - low)>>1);//中间元素的 下标
    if(low < high){
      if(key < a[mid])      return BinarySearch(a, low, mid-1, key);
      else if(key > a[mid]) return BinarySearch(a, mid+1, high, key);
      else return mid;
    }
    return -1;

}

第二题 寻找和为定值的两个数

    输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。
    要求时间复杂度是O(N)。如果有多对数字的和等于输入的数字,输出任意一对即可。
    例如输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。

思路:暴力搜索 时间复杂度 n^2
       其二,二分法(相当于用两个指针),从前后向中间搜索,看情况移动左右指针

代码如下

    // 二分(若无序,先排序后二分),时间复杂度总为O(N log N),空间复杂度为O(1);
    void findTwo2(int a[], int low, int high, int sum){
     //sort(a, a+high+1);   如果数组非有序的,那就事先排好序O(N log N)
             while(low < high){
                    if(a[low] + a[high] > sum)      --high;//太大 right减少
                    else if(a[low] + a[high] < sum) ++low; //太小 low增加
                    else cout << a[low] << a[high] < endl;
             }
            cout << "Can't found" < endl;

}

总结

    不论原序列是有序还是无序,解决这类题有以下三种办法:
    1、二分(若无序,先排序后二分),时间复杂度总为O(N log N),空间复杂度为O(1);
    2、扫描一遍X-S[i] 映射到一个数组或构造hash表,时间复杂度为O(N),空间复杂度为O(N);
    3、两个指针两端扫描(若无序,先排序后扫描),时间复杂度最后为:
       有序O(N),无序O(N log N + N)=O(N log N),空间复杂度都为O(1)。
    所以,要想达到时间O(N),空间O(1)的目标,除非原数组是有序的(指针扫描法),
    不然,当数组无序的话,就只能先排序,后指针扫描法或二分(时间 O(Nlog N),空间O(1)),
    或映射或hash(时间O(N),空间O(N))。时间或空间,必须牺牲一个,达到平衡。
    综上,若是数组有序的情况下,优先考虑两个指针两端扫描法,以达到最佳的时O(N),空O(1)效应。
    否则,如果要排序的话,时间复杂度最快当然是只能达到O(N log N),空间O(1)则不在话下。

举一反三:

    输入:int[]nums int target 
    输出:符合要求的所有情况 
    给定一个int类型的数组,sum2、sum3、sum4分别输出2、3、4个元素的和为target的所有结果,不能重复,并且结果从小到大排序。
    【解法】: 
    三个题目均可以使用双指针的做法。首先对原始数组进行排序,排序的意义为:
    (1)保证最后结果的有序性;
    (2)从小到大的查找是否满足target,
    若小于他,则start指针往前走 ++start,
    若大于他,这end指针往回走 --end; 
    接着第一个指针从最小的元素开始,
    第二个指针从最大的元素开始,通过对和与target进行比较,判断start和end的走向。 
    sum2可以不用循环,只需要两个指针。
    同时sum2还可以有另一种解法,从左到右判断,采用一个集合保存判断过的结果,从集合中查找target-nums[i]是否存在;(n) 
    sum3采用一层循环,start从循环处的下一个元素开始(n`2)
    sum4采用两层循环,第二层循环从第一层循环的i值开始,最内部start从第二层循环的下一个元素开始。(n`3) 

第三题 寻找和为定值的多个数

    题目描述

输入两个整数n和sum,从数列1,2,3…n 中随意取几个数,使其和等于sum,要求将其中所有的可能组合列出来。

01背包问题求解

    解法一
    注意到取n,和不取n个区别即可,考虑是否取第n个数的策略,可以转化为一个只和前n-1个数相关的问题。
    如果取第n个数,那么问题就转化为“取前n-1个数使得它们的和为sum-n”,对应的代码语句就是sumOfkNumber(sum - n, n - 1);
    如果不取第n个数,那么问题就转化为“取前n-1个数使得他们的和为sum”,对应的代码语句为sumOfkNumber(sum, n - 1)。

    0-1背包问题
    0-1背包问题是最基础的背包问题,其具体描述为:
    有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是Ci,得到的价值是Wi。
    求解将哪些物品装入背包可使价值总和最大。
    简单分析下:这是最基础的背包问题,特点是每种物品仅有一件,可以选择放或不放。
    用子问题定义状态:即F[i, v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
    F[i, v] = max{F[i-1, v], F[i-1, v-Ci ] + Wi}
    根据前面的分析,我们不难理解这个方程的意义:
    “将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),
    那么就可以转化为一个只和前 i-1 件物品相关的问题。
    即:
    如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为 F[i-1, v ];
    如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-Ci的背包中”,
    此时能获得的最大价值就是F[i-1, v-Ci]再加上通过放入第i件物品获得的价值Wi。

    list<int>list1;
    void SumOfkNumber(int sum, int n)
    {
        // 递归出口
        if (n <= 0 || sum <= 0)
            return;
        // 输出找到的结果
        if (sum == n)
        {
            // 反转list
            list1.reverse();
            for (list<int>::iterator iter = list1.begin(); iter != list1.end(); iter++)
                cout << *iter << " + ";
            cout << n << endl;
            list1.reverse()//此处还需反转回来
        }
        list1.push_front(n); //放置进背包 当前占了n 前面剩余sum-n空间  典型的01背包问题
        SumOfkNumber(sum - n, n - 1);//“放”n,前n-1个数“填满”sum-n

        list1.pop_front();// 取出来 不放进去 前面还剩余sum的空间
        SumOfkNumber(sum, n - 1); //不“放”n,n-1个数“填满”sum
    }

第四题 最大连续子数组和

    事实上,当我们令currSum为当前最大子数组的和,maxSum为最后要返回的最大子数组的和,当我们往后扫描时,

    对第j+1个元素有两种选择:要么放入前面找到的子数组,要么做为新子数组的第一个元素;
    
    如果currSum加上当前元素a[j]后不小于a[j],则令currSum加上a[j],
    否则currSum重新赋值,置为下一个元素,即currSum = a[j]。
    
    同时,当currSum > maxSum,则更新maxSum = currSum,否则保持原值,不更新。
    即:
    currSum = max(a[j], currSum + a[j])
    maxSum = max(maxSum, currSum)
    举个例子,当输入数组是1, -2, 3, 10, -4, 7, 2, -5,那么,currSum和maxSum相应的变化为:
    currSum: 0 1 -1 3 13 9 16 18 13
    maxSum : 0 1 1 3 13 13 16 18 18

参考代码如下:
int MaxSubArr(int* a, int num){
int currSum = 0;//初始化 当前和 为0
int maxSum = a[0];//初始化 第一个元素为 最大和
for(int j = 0; j < n; ++j ){
currSum = (currSum + a[j] > a[j] ) ? (currSum + a[j]) : a[j];
maxSum = maxSum > currSum ? maxSum : currSum;
}
return maxSum;
}

第五题 跳台阶问题

    一个台阶总共有n 级,如果一次可以跳1 级,也可以跳2 级。
    求总共有多少总跳法,并分析算法的时间复杂度。
解法一:首先考虑最简单的情况,函数递归调用求解。
    如果只有1级台阶,那显然只有一种跳法。
    如果有2级台阶,那就有两种跳的方法了:
            一种是分两次跳,每次跳1级;
            另外一种就是一次跳2级。
    现在我们再来讨论一般情况。我们把n级台阶时的跳法看成是n的函数,记为f(n)。
    当n>2时,第一次跳的时候就有两种不同的选择:
    一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);
    另外一种选择是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。
    因此n级台阶时的不同跳法的总数f(n)=f(n-1)+f(n-2)。
    我们把上面的分析用一个公式总结如下:
            /  1                             n = 1
    f(n)=      2                             n = 2
            \  f(n-1) + f(n-2)               n > 2
    原来上述问题就是我们平常所熟知的Fibonacci数列问题。

可编写代码,如下:

    longn long Fibonacci(unsigned int n){
    int result[] = [0, 1, 2];
    if (n <= 2)  
            return result[n];
    else 
            return Fibonacci(n-1) + Fibonacci(n-2);
    }
那么,如果一个人上台阶可以一次上1个,2个,或者3个呢?这个时候,公式是这样写的:
            / 1                                      n = 1
    f(n)=     2                                      n = 2
              4                                      n = 3       //111, 12, 21, 3
            \ f(n-1) + f(n-2) + f(n-3)               n > 3

可编写代码,如下:

    longn long F(unsigned int n){
    int result[] = [0, 1, 2, 4];
    if (n <= 3)  
            return result[n];
    else 
            return F(n-1) + F(n-2) + F(n-3);
    }

解法二,循环递推公式求解

解法一用的递归的方法有许多重复计算的工作,事实上,我们可以从后往前推,一步步利用之前计算的结果递推。+
初始化时,dp[0]=dp[1]=1,
然后循环递推计算即可:dp[n] = dp[n-1] + dp[n-2],当前解 等于前两次的和

参考代码如下:

    // 1,1,2,3,5,8,13,21..
    int ClimbStairs(int n){// n级台阶,可以一次调1个台阶和2个台阶两种选择,有多少种跳发
            int dp[3] = {1, 1};// 保存当前值 上一次值 上上次值的数组
            if(n <=1) return 1;
            else {
                    for(int i = 2; i <= n; ++i){
                            d[2] = d[1] + d[0];
                            d[0] = d[1];
                            d[1] = d[2];// 更新数组的值
                    }
                }
    }
换硬币问题
    // 想兑换100元钱,有1,2,5,10四种钱,问总共有多少兑换方法。 递推公式 arr[j] += arr[j - dimes[i]];
    const int N = 100;
    int dimes[] = { 1, 2, 5, 10 };
    int arr[N + 1] = { 1 };//存放每一个总钱数可以的兑换次数
    int coinExchange(int n)   //非递归实现       
    {
            for (int i = 0; i < sizeof(dimes) / sizeof(int); ++i)//对于每一个面值
            {
                for (int j = dimes[i]; j <= n; ++j)// 遍历总钱数j ~ n
                {
                    arr[j] = arr[j] + arr[j - dimes[i]];
                    //分为两种情况,
                    //总钱数j,如果没有换当前硬币i,那么是多少?arr[j]
                    //加上,如果换了当前硬币dimes[i],总值减少为 j-dimes[i],此时又是多少种兑换方法?arr[j-dimes[i]]
                }
            }
            return arr[n];
    }

奇偶调序

    输入一个整数数组,调整数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。要求时间复杂度为O(n)。

方法1 利用快排思想 奇偶数分类下标 头指针,向右移动 尾指针,向左移动

    借鉴partition 分治 的实现一,我们可以考虑维护两个指针,
    一个指针指向数组的第一个数字,我们称之为头指针,向右移动;
    一个指针指向最后一个数字,称之为尾指针,向左移动。

    这样,两个指针分别从数组的头部和尾部向数组的中间移动,
    如果第一个指针指向的数字是偶数而
    第二个指针指向的数字是奇数,我们就交换这两个数字。
    因为按照题目要求,最终是为了让奇数排在数组的前面,偶数排在数组的后面,
    所以头指针理应指向的就是奇数,尾指针理应指向的就是偶数,

故当头指针指向的是偶数且尾指针指向的是奇数时,我们就当立即交换它们所指向的数字。

代码实现:

    // 判断是否为奇数
    bool isOddNumber(int n){
    return n & 1 == 1; // 奇数 的最后一个二进制位为1
    }
    
    // void OddEvenChange(int *a, int num){
            if(a == NULL|| num == 0) return;// 空指针 以及长度为0 直接返回
            int* low  = a;           // 头指针
            int* high = a + num - 1; // 尾指针
            while(low < high){
                    if(!isOddNumber(*high) --high;//如果 high指针指向的是偶数,正常,向前移 减小
                    else if(isOddNumber(*low) ++low;//如果 low指针指向的是奇数,正常,向后移 加大
                    else //否则都不正常,交换
                    //swap是STL库函数,声明为void swap(int& a, int& b);
                    swap(*pBegin, *pEnd);
            }
    }

本方法通过头尾两个指针往中间扫描,一次遍历完成所有奇数偶数的重新排列,时间复杂度为O(n)。

荷兰旗 三色球 问题  三类的分配问题

    现有n个红白蓝三种不同颜色的小球,乱序排列在一起,请通过两两交换任意两个球,
    使得从左至右,依次是一些红球(类1)、一些白球(类2)、一些蓝球(类3)。

    通过前面的分析得知,这个问题类似快排中partition过程,只是需要用到三个指针:
    一个前指针begin(指向类1),一个中指针current(指向类2),一个后指针end(指向类3),
    current指针遍历整个数组序列,当:
    current指针所指元素为 类1 时,与begin指针所指的元素交换,而后current++,begin++ ;
    current指针所指元素为 类2 时,不做任何交换(即球不动), 而后current++ ;
    current指针所指元素为 类3 时,与end指针所指的元素交换,  而后,current指针不动(end指向的元素可能为类1 ),end-- 。

参考代码如下:

    while( current<=end )        
    {             
      if( array[current] ==0 )             
       {                 
          swap(array[current],array[begin]);                  
          current++;                  
          begin++;            
       }             
       else if( array[current] == 1 )            
       {                 
          current++;            
       }   

       else //When array[current] =2   
       {               
          swap(array[current],array[end]);                
          end--;            
       }      
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FL1768317410

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值