排序算法总结

Posted 小王子jvm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法总结相关的知识,希望对你有一定的参考价值。

选择排序

定义

一种最简单的排序算法是这样的:假定我们对一个数组进行排序那么,我们可以找到数组中最小的元素,然后与数组的第一个经行交换,如果第一个已经是最小的,那就和自己交换。然后在剩下的元素中找到最小的和第二个进行交换。以此类推,最终得到一个升序的数组。这种方式就叫做选择排序。

冒泡排序的升级版本。

代码实现

@Test
    void select() 
        int nums[] = new int[]9,8,7,6,5,4,3,2,1,0;

        //经行选择排序
        selectSort(nums);
        for (int num : nums) 
            System.out.println(num);
        
    

    private void selectSort(int[] nums) 
        for (int i = 0; i < nums.length; i++) 
            //这种排序需要一个变量来记录当前最小值的位置
            int min = i;
            for (int j = i+1; j < nums.length; j++) 
                if(nums[j] < nums[min])
                    //当前元素比这个小,min指向这个小的
                    min = j;
                
                //把最小的放在剩余元素的最前面
                int tmp = nums[i];
                nums[i] = nums[min];
                nums [min] = tmp;
            
        
    

时间复杂度

这里只考虑最坏的情况。

对于一个长度为N的数组,第一次就需要遍历整个数组,这个时候就是N次,接下来就是N-1,N-2……直到比较完成。所以最多比较次数就是把这些加起来等于N(N-1)/ 2,与就是O(N^2)。

插入排序

定义

斗地主都玩过,摸起来的牌都会一张一张的插入到一个合适的位置,然后正副牌看起来就是井然有序。这种插入的方式就是插入排序。

就比如这个,3比9小,于是插入到9的左边,然后发现,3比7小,又插入到7的左边,直到左边没有比这数小的,总的来说就是一趟过后,左边的都是有序的。

代码实现

@Test
void select() 
    int nums[] = new int[]9,8,7,6,5,4,3,2,1,0;

    //经行选择排序
    insertSort(nums);
    for (int num : nums) 
        System.out.println(num);
    


private void insertSort(int[] nums)
    //外层循环
    for (int i = 1; i < nums.length; i++) 
        for (int j = i;j > 0;j--) 
            //当前比前面的小,插入
            if (nums[j] < nums[j-1])
                //交换
                int tmp = nums[j];
                nums[j] = nums[j-1];
                nums [j-1] = tmp;
             else 
                break;  //没有就后移再次进入循环比较
            
        
    

时间复杂度为:N2/4,就是O(N2),比选择排序好那么一点。

希尔排序

上面的排序每次都是相邻的元素进行比较,导致每次都需要大量的移动,而希尔排序的特点就是,每次使数组变得大致上有序,然后变得有序。每次使一个组有序,总体上就是大致有序,然后缩小这个组直到最后有序。

比如这个,1,7一组,6,5一组,然后这每一个组排个序,整个数组就整体上变得有序很多,然后缩小这个分组,直到最后有序。

代码实现

@Test
void select() 
    int nums[] = new int[]9,8,7,6,5,4,3,2,1,0;

    //经行选择排序
    insertSort(nums);
    for (int num : nums) 
        System.out.println(num);
    


private void shellSort(int[] nums)
    //选择一个增量
    int n = nums.length/2;
    //增量不等于1排序,数组只能大致有序,所以会逐步缩小到1
    while (n > 0) 
        for (int i = n; i < nums.length; i++) 
            for (int j = i; j >= n; j -= n) 
                //比较这两个大小
                if (nums[j] < nums[j-n])
                    //交换
                    int tmp = nums[j];
                    nums[j] = nums[j-n];
                    nums [j-n] = tmp;
                
            
        
        //缩小增量
        n = n / 2;
    

对于这种排序,怎么选择增量就是一个最复杂的问题。

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

用一张图演示归并的过程:

分割归并:

递归

既然是一个分割合并的过程,必然需要两个指针来对着两边进行归并。

public static void main(String[] args) 
    //归并排序,定义一个数组
    int arr[] = new int[]1,5,3,8,7,4,9,2;
    //需要递归的方法
    sort(arr,0,arr.length-1);
    for (int i : arr) 
        System.out.print(i + " ");
    


//方法含义,根据数组进行排序
public static void sort(int arr[], int left, int right)
    //递归结束条件,左指针大于或者等于右指针
    if (left >= right) return;
    //二分的中间值
    int mid = left + (right - left) / 2;
    //否则进行分割
    sort(arr,left, mid);
    sort(arr,mid+1,right);
    //最后才是排序
    merge(arr,left,mid, right);


private static void merge(int[] arr, int left, int mid, int right) 
    //先复制这个数组,这样为了保证原来的数组不被打乱
    int tmp[] = new int[right-left+1];

    int k=0;
    int l = left;
    int r = mid+1;
    //左右指针
    while (l <= mid && r <= right)
        if(arr[l] < arr[r])
            //左边数组小于右边,把左边的放在临时数组中
            tmp[k] = arr[l];
            l++;
         else 
            tmp[k] = arr[r];
            r++;
        
        k++;
    

    //然后把上面没排完的全部放在这个新数组里面
    while (l <= mid) tmp[k++] = arr[l++];
    while (r <= right) tmp[k++] = arr[r++];

    //最后把数组赋值给原来的数组
    for (int i = 0; i < tmp.length; i++) 
        arr[left+i] = tmp[i];
    


其实这是有一点像树的后续遍历,先找到叶子节点(这里先分割成最小单位)。然后在做一个排序处理。

快速排序

快排的性能在所有排序算法里面是最好的,数据规模越大快速排序的性能越优。快排在极端情况下会退化成O(n^2)的算法,因此假如在提前得知处理数据可能会出现极端情况的前提下,可以选择使用较为稳定的归并排序。

一张图解释快排:

仔细看,发现这个过程跟归并排序貌似是一个相反的过程,归并是先分割成最小两个单位,在逐步向上。而快排是先排序成两个分割好的再递进下去。

总结规律:

  • 选择待排序(A)中的任意一个元素pivot,该元素作为基准
  • 将小于基准的元素移到左边,大于基准的元素移到右边(分区操作)
  • A被pivot分为两部分,继续对剩下的两部分做同样的处理
  • 直到所有子集元素不再需要进行上述步骤
public static void main(String[] args) 
    //归并排序,定义一个数组
    int arr[] = new int[]1,5,3,8,7,4,9,2;
    //需要递归的方法
    sort(arr,0,arr.length-1);
    for (int i : arr) 
        System.out.print(i + " ");
    


//方法含义,根据数组进行排序
public static void sort(int arr[], int left, int right)
    int index = 0;
    //递归结束条件,左指针大于或者等于右指针
    if (left < right) 
        //将第一趟排序后的地址拿到
        index = merge(arr, left, right);
        //然后根据这个下标分割这个数组
        sort(arr, left, index - 1);
        sort(arr, index + 1, right);
    


private static int merge(int[] arr, int left, int right) 
    //主要的逻辑代码,双指针左右前进
    int tmp = arr[left];    //保存中轴
    while (left < right) 
        while (left < right && arr[right] >= tmp) 
            //比中轴小,则需要交换到低端,否则直接这个下标后移
            right--;
        
        //进行交换
        arr[left] = arr[right];
        while (left < right && arr[left] <= tmp) 
            //比中轴大就交换到高端,否则就这个下标前移
            left++;
        
        arr[right] = arr[left];
    
    //最后更新这个中轴
    arr[left] = tmp;
    return left;

堆排序

堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种**选择排序,**它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射(层序遍历的一种结构)到数组中就是下面这个样子:

这个数组中蕴含的一个数学公式:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

至此就可以开始着手了解这个堆排序。

基本思想:将待排序构造成一个大顶堆(或者小堆)结构,此时,整个序列的最大值(最小值)一定是堆顶的这个节点,或者说是这个数组的第一个元素。然后把这个值和最后的一个节点交换,这个时候最大或者最小值不久直接搞到最后了嘛,然后把除了最后一个节点的剩下节点再构造成一个堆结构,如此反复,一个有序的数组就得到了。

public static void main(String[] args) 
    int[] nums = 16,7,3,20,17,8;
    headSort(nums);
    for (int num : nums) 
        System.out.print(num + " ");
    


/**
* 堆排序
*/
public static void headSort(int[] list) 
    //构造初始堆,i一定是倒数第二层的一个节点,并且这个节点的右边的节点一定没有孩子节点。
    for (int i = (list.length) / 2 - 1; i >= 0; i--) 
        headAdjust(list, list.length, i);
    
    //排序,将最大的节点放在堆尾,然后从根节点重新调整
    for (int i = list.length - 1; i >= 1; i--) 
        int temp = list[0];
        list[0] = list[i];
        list[i] = temp;
        headAdjust(list, i, 0);
    


/**
* 调整这个节点与左右孩子的关系
* @param list
* @param len
* @param i
*/
private static void headAdjust(int[] list, int len, int i) 

    //tem 是父节点,index是左孩子,k 父节点的位置
    int k = i, temp = list[i], index = 2 * k + 1;

    while (index < len) 
        //index < len 表示有左孩子
        if (index + 1 < len) 
            //找到孩子节点中较大值的下标
            if (list[index] < list[index + 1]) 
                index = index + 1;
            
        

        //这个较大的孩子节点比父节点大,交换。
        if (list[index] > temp) 
            list[k] = list[index];
            k = index;
            index = 2 * k + 1;
         else 
            break;
        
    
    //更新这个孩子节点的值,如果上面没有发生交换,就保持父节点不动
    list[k] = temp;

计数排序

现在假设一种场景,高考一般考生在几百万,有这么多的数据,但是这个数据值的分布呢,0到750这个数据,也就是不论哪个人,分数一定是在这个范围。那么如何知道一个人的排名呢?这个时候就可以统计每个分数的人数,而不是非要把几百万个数据进行一个排序!

核心思想:统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

public static void main(String[] args) 
    //归并排序,定义一个数组
    int arr[] = new int[]1,5,3,8,7,4,0,0,9,2,4,5,1,7,8,9,7,4,4,5,1,2,3,2,6,5,6,9,8,7;
    //需要递归的方法
    int[] sort = sort(arr);
    for (int i : sort) 
        System.out.print(i + " ");
    
    System.out.println();
    for (int i : arr) 
        System.out.print(i+" ");
    



//方法含义,根据数组进行排序
public static int[] sort(int arr[])
    //既然是计数,我们需要一个数组来记录每个数据出现的次数
    int count[] = new int[10];
    //然后遍历这个数组,每个数据当作下标使用
    for (int i = 0; i < arr.length; i++) 
        count[arr[i]]++;
    
    int index = 0; //这个数组的下标,i表示这个数值
    //根据这个计数器得到数据
    for (int i = 0; i < count.length; i++) 
        int tmp = count[i]; //得到这个值的次数
        while (tmp > 0)
            arr[index++] = i;
            tmp--;
        
    
    return count;

前面的排序都是比较大小人后交换位置,这种排序算得上另辟蹊径,直接统计数量然后重新赋值整个数组。但是这种排序大部分只针对这种数字。

基数

以上是关于排序算法总结的主要内容,如果未能解决你的问题,请参考以下文章

基础排序算法总结(代码+图片分析)

算法class11 排序算法-快速排序

排序算法-快速排序

快速排序的总结

快速排序的总结

算法——快速排序算法