查找

二分查找

手写代码

自己语言描述算法

判断查找次数

语言描述

  1. 前提:有已排序数组 A(假设已经做好)

  2. 定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)

  3. 获取中间索引 M = Floor((L+R) /2)

  4. 中间索引的值 A[M] 与待搜索的值 T 进行比较

    ① A[M] == T 表示找到,返回中间索引

    ② A[M] > T,中间值右侧的其它元素都大于 T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找

    ③ A[M] < T,中间值左侧的其它元素都小于 T,无需比较,中间索引右边去找, M + 1 设置为左边界,重新查找

  5. 当 L > R 时,表示没有找到,应结束循环

模式

有序度比较高的数组通常都会考虑这个方法,哪怕是局部有序

代码

public int binarySearch(int []nums,int target){
    int l=0,r=nums.length-1;
    int mid=(l+r)>>>1;
    while(l<=r){
        mid=(r+l)>>>1;
        if(nums[mid]==target){
            return mid;
        }else if(nums[mid]>target){
            r=mid-1;
        }else{
            l=mid+1;
        }
    }
    return -1;
}

查找次数判定

看mid一共有过多少个值,这很好理解

给元素个数:利用公式,取2的对数向下取整

排序

冒泡排序

手写代码

自己语言描述算法

建议一步到位直接优化后的

语言描述

  1. 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
  2. 比较过程中记录最后一次交换的位置,下一轮只需比较到该位置,如果一次交换都没有,则说明数组已经有序,可以直接返回
  3. 重复以上步骤,直到整个数组有序

代码

public void bubbleSort(int [] nums){
    //记录无序区边界,每次从无序区开始排,其后面的都没有交换过,说明已经有序,那么下次就只需排到这里
    int last=-1,n=nums.length-1;
    while(true){
        last=-1;
        for(int j=0;j<n;j++){
            if(nums[j]>nums[j+1]){
                int tmp = nums[j];
                nums[j]=nums[j+1];
                nums[j+1]=tmp;
                last=j;
            }
        }
        //下次比较的边界是最后一次交换的位置
        n=last;
        //没有交换过可以直接返回
        if(n==-1){
            return;
        }
    }
}

选择排序

手写代码

自己语言描述算法

与冒泡比较

语言描述

  1. 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集

  2. 重复以上步骤,直到整个数组有序

代码

public void selectSort(int [] nums){
    for(int i=0;i<nums.length-1;i++){
        //记录最小元素的索引
        int min=i;
        for(int j=i+1;j<nums.length;j++){
            if(nums[min]>nums[j]){
                min=j;
            }
        }
        if(i!=min){
            int tmp=nums[i];
            nums[i]=nums[min];
            nums[min]=tmp;
        }
    }
}

VS冒泡

二者平均时间复杂度都是O(N^2)

选择排序一般要快于冒泡,因为其交换次数少

但如果集合有序度高,冒泡优于选择

冒泡属于稳定排序算法,而选择属于不稳定排序,后者在交换的时候可能把元素带到后面去

插入排序

手写代码

自己语言描述算法

语言描述

将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)

重复以上步骤,直到整个数组有序

代码

public void insertSort(int []nums){
    for(int i=1;i<nums.length;i++){
        int cur=nums[i];
        for(int j=i;j>=1;j--){
            //如果前一个元素比当前元素大,直接将其后移一位
            if(cur<nums[j-1]){
                nums[j]=nums[j-1];
                
            }else{
                nums[j]=cur;
                break;
            }
        }
    }
}

VS选择

二者平均时间复杂度都是O(N^2),插入如果是集合已经有序,则O(N)

插入属于稳定排序算法,而选择属于不稳定排序

大部分情况下,插入都略优于选择

据说插入在小规模排序中是首选

希尔排序

自己语言描述算法

语言描述

  1. 首先选取一个间隙序列,如 (n/2,n/4 … 1),n 为数组长度

  2. 每一轮将间隙相等的元素视为一组,对组内元素进行插入排序,目的有二

    ① 少量元素插入排序速度很快

    让组内值较大的元素更快地移动到后方(改进了插入排序的不足)

  3. 当间隙逐渐减少,直至为 1 时,即可完成排序

代码

public void shellSort(int[] nums){
    int len=nums.length;
    for(int gap=len>>1;gap>=1;gap>>1){
        for(int i=gap;i<len;i++){
            int j=i;
            int cur=nums[i];
            for(j=i;j>=gap;j-=gap){
                if(nums[j-gap]>cur){
                    nums[j]=nums[j-gap];
                }else{
                    break;
                }
            }
            nums[j]=cur;
        }	
    }
}

VS插入

非稳定

如果增量序列选得好,时间复杂度为O(N^1.5)

快速排序

自己语言描述

手写单边循环、双边循环

比较罗穆托和霍尔两种分区方案的性能

语言描述

  1. 每一轮排序选择一个基准点(pivot)进行分区
    1. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
    2. 当分区完成时,基准点元素的位置就是其最终位置
  2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer
  3. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案

分区方案

单边循环快排(lomuto 洛穆托分区方案)

语言描述

最右边元素为基准点

两个指针i,j,i负责维护比基准点小的元素的边界,j负责从左往右找比基准点小的元素

一旦找到,与i交换位置

直到j指针指向基准点,再次与i交换位置

而后i就是基准点在排序完成后所在位置了,也即此次分区的位置

代码
public int[] lomutoQuick(int[] nums){
	int l=0,r=nums.length-1;
    partition(nums,l,r);
    return nums;
}
public void partition(int[] nums,int l,int r){
    if(l>=r){
        return;
    }
    int i=0,j=0;
    int base=nums[r];
    while(j<r){
        while(j<r&&nums[j]>=base){
            
            j++;
        }
        if(j!=i){
			int tmp=nums[i];
			nums[i]=nums[j];
			nums[j]=tmp;
            i++;
        }else{
            j++;
        }
        
    }
    int tmp=nums[i];
	nums[i]=nums[r];
	nums[r]=tmp;
    partition(nums,l,i-1);
    partition(nums,i+1,r);
    return;
}

双边循环快排(不完全等价于 hoare 霍尔分区方案)

语言描述

最左边元素为基准点

两个指针i,j,i从左往右找大于基准点的元素,j负责从右往左找大于基准点的元素

先找j,再找i

找到后交换位置

直到两指针相碰,将基准点元素与之交换位置

j即为此次分区位置

代码
public int [] doubleQuick(int[] nums){
    int l=0,r=nums.length-1;
    partition(nums,l,r);
    return nums;
}
public void partition(int[] nums,int l,int r){
    if(l>=r){
        return;
    }
    int base=nums[l];
    int i=l,j=r;
    while(i<j){
        while(i<j&&nums[j]>=base)j--;
        while(i<j&&nums[i]<=base)i++;
        int tmp=nums[i];
        nums[i]=nums[j];
        nums[j]=tmp;
    }
    int tmp=nums[i];
        nums[i]=nums[l];
        nums[l]=tmp;
    partition(nums,i+1,r);
    partition(nums,l,i-1);
    return;
}

特点

快,平均时间复杂度是 O(nlog_2⁡n ),平均来讲分区分logn次,每次都会操作n个元素,最坏时间复杂度 O(n^2),最坏情况下分区会分n次,那种完全逆序的情况

不稳定

数据量较大时优势极其明显

罗穆托和霍尔分区性能比较

霍尔的移动次数平均来讲比洛穆托少3倍