算法筑基(三)之数组

137 阅读4分钟

在开发的时候我们最熟悉,最常用的就是数组了。对Array或ArrayList相关的api我们一定是了然于胸,还有数组在内存是连续的,由于是连续的所以它查找快,增删慢,这些大家都已经耳熟能详了。

但是在面试题中遇到的一些对数组的排序,查找的问题,往往还是不能得心应手的解决,所以我想通过总结一下对数组的排序和查找的知识,增加对数组理解,为后续更复杂的问题打下基础。

首先我们说一下如何在一个数组中查找我们想要的元素

一、顺序查找

这个就不用多说了,我们从头到尾遍历每一个元素,直到找到相等的元素,所以它的时间复杂度是O(n)

二、二分查找:

他有一个前提,数组是有序的,这个思想非常简单,大家一看就能明白,

关于二分查找的动图演示可以通过这个网站去查找 www.cs.usfca.edu/~galles/vis…

关于二分查找的算法如下:

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; 

    while(left <= right) { 
        //这里注意不用(right + left)/2,可以避免超过Integer.MAX_VALUE
        int mid = ((right - left) / 2)+ left;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
        }
    return -1;
}

由于每次循环需要的数据量都会减半,所以它的时间复杂度是O(logn)

对于查找算法还有很多,这里我主要想记录一些二分查找法的基本写法,以及细节。后续如果碰到算法题需要其它查找方法再补充。


刚才说到二分查找的前提是数组有序,那如何给一个无序数组进行排序呢。

排序算法分为

  • 比较类排序
    • 交换排序
      • 冒泡排序
        
      • 快速排序
        
    • 插入排序
      • 简单插入排序
        
      • 希尔排序
        
    • 选择排序
      • 简单选择排序
        
      • 堆排序
        
    • 归并排序
      • 二路归并排序
        
      • 多路归并排序
        
  • 非比较类排序
    • 计数排序
    • 桶排序
    • 基数排序

各个排序算法的时间复杂度如下:

image.png

参考链接: www.cnblogs.com/onepixel/p/… 里面有各个算法讲解与动画演示。

我认为对于每种排序至少掌握一种,能非常熟练的写出来,而且能明确的分析它的时间复杂度为什么是这个结果。而不是靠死记硬背,需要多多思考和练习。先练习了基本功,后续我们再找几道实战的问题去巩固它。


2022/3/20 我们看一道leetcode的例题:

合并两个有序数组

这道题个人认为比较好理解的就是双指针法:

首先利用两个指针指向数组起始位置,

1.比较两个指针所对应数字的大小,谁小就将谁取出放入到新数组的第一位

2.并将指针位置向后移动一位。

3.重复上面的步骤

直到其中一个数组的指针指向末尾,那么另一个数组的数字一定比之前排好序的数字都大,直接将剩余部分追加到新数组的后面。

没有找到合适的动图,如果有动图的话可能会更加直观。根据这个思路我们的代码实现如下:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {

        int p=0,q=0,newIndex = 0;
        int[] newSort = new int[m + n];
        while (p<m && q<n){
           int min =  nums1[p] <= nums2[q] ? nums1[p++] : nums2[q++];
           newSort[newIndex++] = min;
        }

        while (p<m){
            newSort[newIndex++] = nums1[p];
            p++;
        }

        while (q<n){
            newSort[newIndex++] = nums2[q];
            q++;
        }


        for (int i = 0; i < m + n; i++) {
            nums1[i] = newSort[i];
        }
    }
}

官方的题解中nums1的空间是两个数组有效长度的和,所以指针的位置是否已经到了结尾需要根据m去判断,而且最后我们要把新的数组再重新填回nums1,但是道理是一样的。官方题解中还有一种更巧妙的逆序双指针感兴趣的可以去看看。


我们理解了合并有序的数组的方法,可以帮助我们更好的理解分治排序因为它是分治排序的其中一个核心步骤。所谓分治排序就是利用分治思想,即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。实现步骤分为:

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

代码实现如下:(这是一段伪代码)

 int[] divideConquerSort(int[] arr){
        int n = arr.length;
        if(n<2){
            return arr;
        }

        int middle = n/2;
        //1.拆分数组
        int[] left = divideArray(arr,0,middle);
        int[] right = divideArray(arr,middle,n);
        //2.合并每个子序列
        return merge(divideConquerSort(left),divideConquerSort(right));
    }

第二步就是上面讲到的合并两个有序数组。

还有一个采用分治思想极其高效的排序算法就是选择排序,感兴趣的可以自行查阅,暂时我们先掌握这两种算法就可以了。