十大排序算法-Java实现

1、冒泡排序

1、简介

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

2、原理

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

动图演示:

3、实现

public class 冒泡排序 {
​
    public static int[] BubbleSort(int[] arr){
​
        for(int i = 0; i < arr.length; i++){
            //逐步将最大值冒泡到数组底部
            for(int j = 0; j < arr.length - 1 - i; j++){
                if(arr[j] > arr[j + 1]){
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
​
                }
            }
        }
        return arr;
    }
​
    public static void main(String[] args) {
        int[] arr = {2, 45, 6, 12, 3, 453, 6};
        int[] newarr = BubbleSort(arr);
        for(int num : newarr){
            System.out.println(num);
        }
    }
}

4、复杂度

  • 平均时间复杂度:O(N^2)

  • 最佳时间复杂度:O(N)

  • 最差时间复杂度:O(N^2)

  • 空间复杂度:O(1)

  • 排序方式:In-place

  • 稳定性:稳定

解析说明:

冒泡排序涉及相邻两两数据的比较,故需要嵌套两层 for 循环来控制;

外层循环 n 次,内层最多时循环 n – 1次、最少循环 0 次,平均循环(n-1)/2;

所以循环体内总的比较交换次数为:n*(n-1) / 2 = (n^2-n)/2 ;

按照计算时间复杂度的规则,去掉常数、去掉最高项系数,其复杂度为O(N^2) ;

最优的空间复杂度为开始元素已排序,则空间复杂度为 0;

最差的空间复杂度为开始元素为逆排序,则空间复杂度为 O(N);

平均的空间复杂度为O(1) .

注:

n:数据规模 k:”桶”的个数 In-place:占用常数内存,不占用额外内存 Out-place:占用额外内存

2、插入排序

1、简介

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。插入排序操作类似于摸牌并将其从大到小排列。每次摸到一张牌后,根据其点数插入到确切位置。

2、原理

插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

具体以一组数字来说操作说明:

例如我们有一组数字:{5,2,4,6,1,3},我们要将这组数字从小到大进行排列。 我们从第二个数字开始,将其认为是新增加的数字,这样第二个数字只需与其左边的第一个数字比较后排好序;在第三个数字,认为前两个已经排好序的数字为手里整理好的牌,那么只需将第三个数字与前两个数字比较即可;以此类推,直到最后一个数字与前面的所有数字比较结束,插入排序完成。

3、实现

① 从第一个元素开始,该元素可以认为已经被排序 ② 取出下一个元素,在已经排序的元素序列中从后向前扫描 ③如果该元素(已排序)大于新元素,将该元素移到下一位置 ④ 重复步骤③,直到找到已排序的元素小于或者等于新元素的位置 ⑤将新元素插入到该位置后 ⑥ 重复步骤②~⑤

public class 插入排序 {
    public static int[] InsertSort(int[] arr){
        // 检查数据合法性
        if(arr == null || arr.length <= 0){
            return null;
        }
        for(int i = 1; i < arr.length; i++){
            int tmp = arr[i];
            int j;
            for(j = i - 1; j >= 0; j--){
                //如果比tmp大把值往后移动一位
                if(arr[j] > tmp){
                    arr[j + 1] = arr[j];
                }
                else{
                    break;
                }
            }
            arr[j + 1] = tmp;
        }
        return arr;
    }
​
    public static void main(String[] args) {
        int []arr={2,45,6,12,3,453,6};
        int [] newarr=InsertSort(arr);
        for(int num:newarr){
            System.out.println(num);
        }
    }
}

4、复杂度

平均时间复杂度:O(N^2) 最差时间复杂度:O(N^2) 空间复杂度:O(1) 排序方式:In-place 稳定性:稳定

如果插入排序的目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况:

(1) 最好情况:序列已经是升序排列,在这种情况下,需要进行的比较操作需(n-1)次即可。 (2) 最坏情况:序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。

插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(N^2)。

最优的空间复杂度为开始元素已排序,则空间复杂度为 0;

最差的空间复杂度为开始元素为逆排序,则空间复杂度最坏时为 O(N);

平均的空间复杂度为O(1)

注:
n:数据规模
k:”桶”的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存

3、Shell排序

1、简介

希尔排序也是一种插入排序算法,也叫作缩小增量排序,是直接插入排序的一种更高效的改进算法。

2、原理

希尔排序的基本思想是:把待排序的数列按照一定的增量分割成多个子数列。但是这个子数列不是连续的,而是通过前面提到的增量,按照一定相隔的增量进行分割的,然后对各个子数列进行插入排序,接着增量逐渐减小,然后仍然对每部分进行插入排序,在减小到1之后直接使用插入排序处理数列。

需要特别强调,这里选择增量的要求是每次都要减少,直至最后一次变为1为止。

例:

在本实例中的首选增量为 n/2,n 为待排序的数列的长度,并且每次的增量都为上一次的 1/2。待排序的数列为 588、392、898、115、306、62、909、902、789、234,有 10 个数。我们首选增量为 10/2 即 5,进行如图1所示的分块。

图 1 增量为 5 时的分块

由于增量为 5,所以把原待排序的数列按照增量划分为 5 组,每组实际上都是以增量为间隔的(数组下标为0的元素对应下标为 5 的元素,1 对应 6、2 对应 7,等等)。

之后对每组进行插入排序,其实就是将后一个元素与前一个元素进行比较,看看是否需要交换(当然,还是应该按照插入排序的步骤来进行,就是把后一个元素拿出来,与前一个元素进行比较,看看是否需要移动前面的元素,如果需要移动,则把第 1 个元素后移,然后把拿出来的元素放到前面去;若不需要移动,则不需要进行其他操作)。

分别对每组元素进行插入排序之后的结果如图 2 所示。

图 2 对 5 组数据进行插入排序操作后的结果

我们发现,实际上在这组数列中只有两组数据进行了移动操作。至此,第 1 趟排序完成。现在的待排序的数列变成了 62、392、898、115、234、588、909、902、789、306。接下来我们该缩减增量了,按照之前的规则,这次的增量应该是 5/2,也就是 2,于是出现了如图 3 所示的划分结果。

图 3 增量为 2 时的第 2 趟划分

现在的待排序的数列的增量为 2,所以每隔一个元素进行分组(也就是数组下标为 0、2、4、6、8 的元素为一组,数组下标为 1、3、5、7、9 的元素为一组),当前的数列被划分为两组,继续对每组数列进行插入排序。

这里就不再复述插入排序的步骤了,大家应该可以轻易地完成对两组数据的插入排序了。第 2 趟排序的结果如图 4 所示。现在的待排序的数列变为 62、115、234、306、789、392、898、588、909、902。

图 4 第 2 趟排序的结果

我们发现,每趟排序都会使数组整体更趋于有序了。

接下来对增量继续按照规则除以 2,得到 1,说明这时该对上一趟完成排序的数列进行直接插入排序了。第3趟排序的结果就是最终结果:62、115、234、306、392、588、789、898、902、909,至此希尔排序结束。

3、实现

public class Shell排序 {
    public static int[] ShellSotr(int[] arr){
        int temp;
        //gap为增量,把原待排序的数列按照增量划分为gap组,每组实际上都是以增量为间隔的,并且每次的增量都为上一次的 1/2
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //将原数组划分为gap组,例如划分为[0,gap], [1,gap], ..., [gap-1,arr.length-1]下标的gap组
            for (int i = gap; i < arr.length; i++) {
                //对每组进行插入排序,其实就是将后一个元素与前一个元素进行比较,然后做排序
                for (int j = i; j >= gap; j -= gap) {
                    if (arr[j - gap] > arr[j]) {
                        temp = arr[j - gap];
                        arr[j - gap] = arr[j];
                        arr[j] = temp;
                    }
                }
            }
        }
        return arr;
    }
​
    public static void main(String[] args) {
        int []arr={2,45,6,12,3,453,6};
        int [] newarr=ShellSotr(arr);
        for(int num:newarr){
            System.out.println(num);
        }
    }
}

4.、复杂度

平均时间复杂度:O(Nlog2N) 最佳时间复杂度: 最差时间复杂度:O(N^2) 空间复杂度:O(1) 稳定性:不稳定 复杂性:较复杂

希尔排序的效率取决于增量值gap的选取,时间复杂度并不是一个定值。

开始时,gap取值较大,子序列中的元素较少,排序速度快,克服了直接插入排序的缺点;其次,gap值逐渐变小后,虽然子序列的元素逐渐变多,但大多元素已基本有序,所以继承了直接插入排序的优点,能以近线性的速度排好序。

最优的空间复杂度为开始元素已排序,则空间复杂度为 0;最差的空间复杂度为开始元素为逆排序,则空间复杂度为 O(N);平均的空间复杂度为O(1)希尔排序并不只是相邻元素的比较,有许多跳跃式的比较,难免会出现相同元素之间的相对位置发生变化。比如上面的例子中希尔排序中相等数据5就交换了位置,所以希尔排序是不稳定的算法。

4、选择排序

1、简介

选择排序(Selection sort)是一种简单直观的排序算法。

2、原理

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面,或者将最大值放在最后面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择,每一趟从前往后查找出无序区最小值,将最小值交换至无序区最前面的位置。

动图演示:

红色表示当前最小值,黄色表示已排序序列,绿色表示当前位置。

具体的我们以一组无序数列{20,40,30,10,60,50}为例分解说明,如下图所示:

3、实现

① 第一轮从下标为 1 到下标为 n-1 的元素中选取最小值,若小于第一个数,则交换 ② 第二轮从下标为 2 到下标为 n-1 的元素中选取最小值,若小于第二个数,则交换 ③ 依次类推下去……

public class 选择排序 {
​
    public static int[] SelectSort(int[]arr){
        int min;
        for (int i = 0; i < arr.length - 1; i++) {
            min = i;
            //寻找最小值的下标
            for (int j = i + 1; j < arr.length; j++){
                if (arr[min] > arr[j])
                    min = j;
            }
            //交换位置
            int temp = arr[min];
            arr[min] = arr[i];
            arr[i] = temp;
        }
        return arr;
    }
​
    public static void main(String[] args) {
        int []arr={2,45,6,12,3,453,6};
        int [] newarr=SelectSort(arr);
        for(int num:newarr){
            System.out.println(num);
        }
    }
}

4、复杂度

平均时间复杂度:O(N^2) 最佳时间复杂度:O(N^2) 最差时间复杂度:O(N^2) 空间复杂度:O(1) 排序方式:In-place 稳定性:不稳定

选择排序的交换操作介于和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。

比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N = (n-1) + (n-2) +…+ 1 = n x (n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。

5、快速排序

1、简介

快速排序,又称划分交换排序(partition-exchange sort)。

2、原理

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

3、实现

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

步骤:

① 从数列中挑出一个元素,称为 “基准”(pivot), ② 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。 ③ 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

动图演示:

public class 快速排序 {
    public static void QuickSort(int[] arr, int low, int hight) {
        if(low < hight){
            int privotIndex = partition(arr, low, hight);
            QuickSort(arr, low, privotIndex - 1);
            QuickSort(arr, privotIndex + 1, hight);
​
        }
​
    }
​
    public static int partition(int[] array, int low, int hight){
        int privot = array[low];//基准数
        //将数值以基准数分区
        while(low < hight){
            while(low < hight && array[hight] >= privot){
                hight--;
            }
            array[low] = array[hight];
            while(low < hight && array[low] <= privot){
                low++;
            }
            array[hight] = array[low];
        }
        //此时low和high两个指针重合了!
        array[low] = privot;
        //返回新的基准下标
        return low;
    }
}

4、复杂度

平均时间复杂度:O(NlogN) 最佳时间复杂度:O(NlogN) 最差时间复杂度:O(N^2) 空间复杂度:根据实现方式的不同而不同

6、归并排序

1、简介

归并排序,是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。

2、原理

归并排序是用分治思想,分治模式在每一层递归上有三个步骤:

  • 分解(Divide):将n个元素分成个含n/2个元素的子序列。

  • 解决(Conquer):用合并排序法对两个子序列递归的排序。

  • 合并(Combine):合并两个已排序的子序列已得到排序结果。

3、实现

2.3.1 迭代法

① 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 ② 设定两个指针,最初位置分别为两个已经排序序列的起始位置 ③ 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 ④ 重复步骤③直到某一指针到达序列尾 ⑤ 将另一序列剩下的所有元素直接复制到合并序列尾

2.3.2 递归法

① 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素 ② 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素 ③ 重复步骤②,直到所有元素排序完毕

归并排序演示:

具体的我们以一组无序数列{14,12,15,13,11,16}为例分解说明,如下图所示:

上图中首先把一个未排序的序列从中间分割成2部分,再把2部分分成4部分,依次分割下去,直到分割成一个一个的数据,再把这些数据两两归并到一起,使之有序,不停的归并,最后成为一个排好序的序列。

迭代法:

递归法:

public class 归并排序 {
​
    public static int[] MergeSort(int[] arr){
        if(arr.length < 2){
            return arr;
        }
        int mid = arr.length / 2;
        int[] left = Arrays.copyOfRange(arr,0,mid);
        int[] right = Arrays.copyOfRange(arr,mid,arr.length);
        //递归
        return Merge(MergeSort(left),MergeSort(right));
    }
    public static int[] Merge(int[] left,int[] right){
        int[] res= new int[left.length + right.length];
        //使用index作为res的下标,i作为left下标,j作为right下标
        for(int index = 0, i = 0, j = 0; index < res.length; index++){
            //若i大于等于left长度,则说明left中的数据都存放到了res中,此时将right剩余的数据依次存入res
            if(i >= left.length){
                res[index] = right[j++];
            }
            //若j大于等于right长度,则说明right中的数据都存放到了res中,此时将left剩余的数据依次存入res
            else if(j >= right.length){
                res[index] = left[i++];
            }
            //比较left和right中的数据大小,并对其做排序
            else if(left[i] > right[j]){
                res[index] = right[j++];
            }
            else{
                res[index] = left[i++];
            }
        }
        return res;
    }
​
    public static void main(String[] args) {
        int []arr={2,45,6,12,3,453,6};
        int [] newarr=MergeSort(arr);
        for(int num:newarr){
            System.out.println(num);
        }
    }
}

4、复杂度

平均时间复杂度:O(nlogn) 最佳时间复杂度:O(n) 最差时间复杂度:O(nlogn) 空间复杂度:O(n) 排序方式:In-place 稳定性:稳定

不管元素在什么情况下都要做这些步骤,所以花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度及平均时间复杂度都是一样的为:O( nlogn )

归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)。

归并排序算法中,归并最后到底都是相邻元素之间的比较交换,并不会发生相同元素的相对位置发生变化,故是稳定性算法。

7、堆排序

二叉树:

  1. 特点是与每个结点关联的子结点至多有两个(可为0,1,2),即一个结点至多有两棵子树。

  2. 二叉树的两棵子树分别称作它的左子树和右子树,即:子树有左右之分(因此二叉树与树有不同结构,不是树的特殊情况)。

满二叉树:

树中每个分支结点(非叶结点)都有两棵非空子树

如图:

完全二叉树:

对于一个树高为h的二叉树,如果其第0层至第h-1层的节点都满。如果最下面一层节点不满,则所有的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵完全二叉树。

如图:

完全二叉树最重要的性质:如果n个节点的完全二叉树的节点按照层次并按从左到右的顺序从0开始编号,则有:

  • 序号为0的节点是根

  • 对于i>0,其父节点的编号为(i-1)/2

  • 2·i+1<n,其左子节点的序号为2·i+1,否则没有左子节点。

  • 2·i+2<n,其右子节点的序号为2·i+2,否则没有右子节点。

堆:

堆一般指的是二叉堆,顾名思义,二叉堆是完全二叉树或者近似完全二叉树。

1、堆的性质

① 是一棵完全二叉树

② 每个节点的值都大于或等于其子节点的值,为最大堆;反之为最小堆。

如图为最小堆:

2、堆的存储

一般用数组来表示堆,下标为 i 的结点的父结点下标为(i-1)/2;其左右子结点分别为 (2i + 1)、(2i + 2);堆排序(完全二叉树)最后一个非叶子节点为2/n-1,n为长度(节点个数)。

3、堆的操作

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点 ② 创建最大堆(Build_Max_Heap):将堆所有数据重新排序 ③ 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

1、简介

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

2、原理

利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。

① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点 ② 依次将根节点与待排序序列的最后一个元素交换 ③ 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列

3、实现

① 先将初始的R[0…n-1]建立成最大堆,此时是无序堆,而堆顶是最大元素。 ② 再将堆顶R[0]和无序区的最后一个记录R[n-1]交换,由此得到新的无序区R[0…n-2]和有序区R[n-1],且满足R[0…n-2].keys ≤ R[n-1].key ③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。 ④ 直到无序区只有一个元素为止。

动图演示:

堆排序算法的演示。首先,将元素进行重排,以匹配堆的条件。图中排序过程之前简单的绘出了堆树的结构。

分步解析说明

实现堆排序需要解决两个问题:

1、如何由一个无序序列建成一个堆? 2、如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

假设给定一个组无序数列{100,5,3,11,6,8,7},带着问题,我们对其进行堆排序操作进行分步操作说明。

1、创建最大堆

①首先我们将数组我们将数组从上至下按顺序排列,转换成二叉树:一个无序堆。每一个三角关系都是一个堆,上面是父节点,下面两个分叉是子节点,两个子节点俗称左孩子、右孩子;

②转换成无序堆之后,我们要努力让这个无序堆变成最大堆(或是最小堆),即每个堆里都实现父节点的值都大于任何一个子节点的值。

③从最后一个堆开始,即左下角那个没有右孩子的那个堆开始;首先对比左右孩子,由于这个堆没有右孩子,所以只能用左孩子,左孩子的值比父节点的值小所以不需要交换。如果发生交换,要检测子节点是否为其他堆的父节点,如果是,递归进行同样的操作。

④第二次对比红色三角形内的堆,取较大的子节点,右孩子8胜出,和父节点比较,右孩子8大于父节点3,升级做父节点,与3交换位置,3的位置没有子节点,这个堆建成最大堆。

⑤对黄色三角形内堆进行排序,过程和上面一样,最终是右孩子33升为父节点,被交换的右孩子下面也没有子节点,所以直接结束对比。

⑥最顶部绿色的堆,堆顶100比左右孩子都大,所以不用交换,至此最大堆创建完成。

2、堆排序(最大堆调整)

①首先将堆顶元素100交换至最底部7的位置,7升至堆顶,100所在的底部位置即为有序区,有序区不参与之后的任何对比。

②在7升至顶部之后,对顶部重新做最大堆调整,左孩子33代替7的位置。

③在7被交换下来后,下面还有子节点,所以需要继续与子节点对比,左孩子11比7大,所以11与7交换位置,交换位置后7下面为有序区,不参与对比,所以本轮结束,无序区再次形成一个最大堆。

④将最大堆堆顶33交换至堆末尾,扩大有序区;

⑤不断建立最大堆,并且扩大有序区,最终全部有序。

/*
可以将堆看做是一个完全二叉树。并且,每个结点的值都大于等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于等于其左右孩子结点的值,称为小顶堆。
堆排序(Heap Sort)是利用堆进行排序的方法。其基本思想为:
将待排序列构造成一个大顶堆(或小顶堆),整个序列的最大值(或最小值)就是堆顶的根结点,
将根节点的值和堆数组的末尾元素交换,此时末尾元素就是最大值(或最小值),然后将剩余的n-1个序列重新构造成一个堆,
这样就会得到n个元素中的次大值(或次小值),如此反复执行,最终得到一个有序序列。
 */
public class 堆排序 {

    static int len;

    public static int[] HeapSort(int[] arr){
        len = arr.length;
        if(len < 1) return arr;
        //1.构建一个最大堆
        BuildMaxHeap(arr);
        //2.循环将堆首位(最大值)与末位交换,然后再重新调整最大堆
        while(len > 0){
            swap(arr,0,len-1);
            len--;
            adjustHeap(arr,0);//调整最大堆
        }
        return arr;
    }

    /**
     * 建立最大堆
     */
    public static void BuildMaxHeap(int[] arr){
        //从最后一个非叶子节点开始向上构造最大堆
        //for循环这样写会更好一点:i的左子树和右子树分别为2i+1和2i+2
        for(int i = (len/2)-1; i >= 0; i--){
            //最后一个非叶子节点:len/2 -1 ;后面就全是叶子节点了
            adjustHeap(arr, i);
        }
    }

    /**
     * 调整最大堆
     */
    public static void adjustHeap(int[] arr, int i){
        int maxIndex = i;
        //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
        if(i * 2 + 1 < len && arr[i * 2 + 1] > arr[maxIndex]){
            maxIndex = i * 2 + 1;
        }
        //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
        if(i * 2 + 2 < len && arr[i * 2 + 2] > arr[maxIndex]){
            maxIndex = i * 2 + 2;
        }
        //如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父亲节点交换的位置
        if(maxIndex != i){
            swap(arr, maxIndex, i);
            adjustHeap(arr, maxIndex);
        }
    }

    /**
     * 交换元素位置
     */
    public static void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr={2,45,6,12,3,453,6};
//        int[] arr = {2,45,6};
        int[] newarr = HeapSort(arr);
        for(int num : newarr){
            System.out.println(num);
        }
        System.out.println("==============");
        BuildMaxHeap(arr);
        for(int num:arr){
            System.out.println(num);
        }

    }

}

4、复杂度

  • 平均时间复杂度:O(nlogn)

  • 最佳时间复杂度:O(nlogn)

  • 最差时间复杂度:O(nlogn)

  • 稳定性:不稳定

堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。

8、计数排序

1、简介

计数排序(Counting sort)是一种稳定的线性时间排序算法。

2、原理

计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),然后进行分配、收集处理:

分配。扫描一遍原始数组,以当前值-minValue作为下标,将该下标的计数器增1。 ② 收集。扫描一遍计数器数组,按顺序把值收集起来。

3、实现

实现逻辑:

① 找出待排序的数组中最大和最小的元素 ② 统计数组中每个值为i的元素出现的次数,存入数组C的第i项 ③ 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) ④ 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

动图演示:

举个例子,假设有无序数列nums=[2, 1, 3, 1, 5], 首先扫描一遍获取最小值和最大值,maxValue=5, minValue=1,于是开一个长度为5的计数器数组counter

(1) 分配 统计每个元素出现的频率,得到counter=[2, 1, 1, 0, 1],例如counter[0]表示值0+minValue=1出现了2次。 (2) 收集 counter[0]=2表示1出现了两次,那就向原始数组写入两个1,counter[1]=1表示2出现了1次,那就向原始数组写入一个2,依次类推,最终原始数组变为[1,1,2,3,5],排序好了。

代码实现:

//只能对有确定范围的整数进行排序
public class 计数排序 {
    public static int[] CountingSort(int[] arr){
        if(arr.length == 0){
            return arr;
        }
        int min = arr[0];
        int max = arr[0];
        int bias;
        //找出最大值和最小值
        for(int i = 0; i < arr.length; i++){
            if(arr[i] < min){
                min = arr[i];
            }
            if(arr[i] > max){
                max = arr[i];
            }
        }
        //偏差:
        bias = 0 - min; //因为有可能最小值不是从0开始的
        //新开一个数组,arr[i]的值作为索引j,bucket[j]的值作为arr[i]出现次数
        int[] bucket = new int[max - min + 1];
        //初始化
        /**
         * Arrays.fill(int[] a, int fromIndex,int toIndex, int val):
         * 将指定的int值val分配给指定int型数组指定范围中的每个元素。填充的范围从索引fromIndex(包括)
         * 一直到索引toIndex(不包括),如果不写fromIndex和toIndex,则填充所有位置。
         */
        Arrays.fill(bucket,0);
        for(int i = 0; i < arr.length; i++){
            bucket[arr[i] + bias] += 1;
        }
        int index = 0;
        //将bucket的索引输出到arr中作为arr的值,将bucket的值作为arr值出现次数
        for(int i = 0; i < bucket.length; i++){
            int len = bucket[i];
            while(len > 0){
                arr[index++] = i - bias;
                len--;
            }
        }
        return arr;
    }

    public static void main(String[] args) {
        int[] arr = {2,-45,-6,12,3,453,6};
        int[] newarr = CountingSort(arr);
        for(int num : newarr){
            System.out.println(num);
        }

    }
}

4、复杂度

平均时间复杂度:O(n + k) 最佳时间复杂度:O(n + k) 最差时间复杂度:O(n + k) 空间复杂度:O(n + k)

9、桶排序

1、简介

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

2、原理

桶排序的思想近乎彻底的分治思想

桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。

然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。

接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。

补充: 映射函数一般是 f = array[i] / k; k^2 = n; n是所有元素个数

为了使桶排序更加高效,我们需要做到这两点:

1、在额外空间充足的情况下,尽量增大桶的数量; 2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中;

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

3、实现

  • 设置一个定量的数组当作空桶子。

  • 寻访序列,并且把项目一个一个放到对应的桶子去。

  • 对每个不是空的桶子进行排序。

  • 从不是空的桶子里把项目再放回原来的序列中。

动图演示:

分步骤图示说明:设有数组 array = [63, 157, 189, 51, 101, 47, 141, 121, 157, 156, 194, 117, 98, 139, 67, 133, 181, 13, 28, 109],对其进行桶排序:

代码实现:

public class 桶排序 {

    public static ArrayList<Integer> BucketSort(ArrayList<Integer> arr, int bucketSize){
        if(arr == null || arr.size() < 2){
            return arr;
        }

        //获得最大和最小值
        int max = arr.get(0);
        int min = arr.get(0);
        for(int i = 0; i < arr.size(); i++){
            if(arr.get(i) > max){
                max = arr.get(i);
            }
            if(arr.get(i) < min){
                min = arr.get(i);
            }
        }

        //桶数量
        int bucketCount = (max - min) / bucketSize + 1;
        //用来构造多个桶,并存放数据到桶中
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        //存放最终结果
        ArrayList<Integer> resultArr = new ArrayList<>();
        //构造桶
        for(int i = 0; i < bucketCount; i++){
            bucketArr.add(new ArrayList<Integer>());
        }
        //往桶里塞元素
        //使用(arr[i]-min)/bucketSize来计算将arr[i]存放到哪个桶中,可以达到排序的目的
        for(int i = 0; i < arr.size(); i++){
            bucketArr.get((arr.get(i) - min) / bucketSize).add(arr.get(i));
        }

        //递归输出,递归的目的是构造出更多的桶,让每个不同的数值都能对应放入一个桶中,以实现排序的目的
        for(int i = 0; i < bucketCount; i++){
            //若bucketSize=1,则说明有arr.size()个桶,arr中的每个数据都能被排好序分配到一个桶中,且无法继续往下分配桶。此时可以返回递归结果
            if(bucketSize == 1){
                for(int j = 0; j < bucketArr.get(i).size(); j++){
                    resultArr.add(bucketArr.get(i).get(j));
                }

            }
            //若bucketSize!=1,则说明桶中的数据尚未排序完毕,只有当bucketSize=1时,才说明排序完毕
            else{
                //若bucketCount=1,则说明只有一个桶,因此无法对数据排序,此时bucketSize值过大,需要减小
                if(bucketCount == 1){
                    bucketSize--;
                }
                //递归
                ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
                for(int j = 0; j < temp.size(); j++){
                    resultArr.add(temp.get(j));
                }
            }
        }
        return resultArr;
    }

    public static void main(String[] args) {
        ArrayList<Integer> arr = new ArrayList<Integer>(){{
            add(2);
            add(45);
            add(6);
            add(12);
            add(3);
            add(453);
            add(6);
        }};
        ArrayList<Integer> newarr = BucketSort(arr, 7);
        for(int num:newarr){
            System.out.println(num);
        }
    }
}

4、复杂度

平均时间复杂度:O(n + k) 最佳时间复杂度:O(n + k) 最差时间复杂度:O(n ^ 2) 空间复杂度:O(n * k) 稳定性:稳定

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

10、基数排序

1、简介

基数排序(Radix sort)是一种非比较型整数排序算法。

2、原理

原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

  • MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序

  • LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序

3、实现

① 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。 ② 从最低位开始,依次进行一次排序。 ③ 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

分步图示说明:设有数组 array = {53, 3, 542, 748, 14, 214, 154, 63, 616},对其进行基数排序:

在上图中,首先将所有待比较数字统一为统一位数长度,接着从最低位开始,依次进行排序。

  • 按照个位数进行排序。

  • 按照十位数进行排序。

  • 按照百位数进行排序。

排序后,数列就变成了一个有序序列。

代码实现:

注意:该代码无法对负数排序

public class 基数排序 {

    public static int[] RadixSort(int[] array) {
        if (array == null || array.length < 2)
            return array;

        // 1.先算出最大数的位数;
        //最大数值
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            max = Math.max(max, array[i]);
        }
        //最大位数
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        //用于后面取出每个位数的值
        int mod = 10, div = 1;
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
        //使用桶排序
        //构建bucketList线性表,即构造桶
        //此处构造10个桶是因为位数的值只有0-9这十个数,每个数对应放入一个桶中
        for(int i = 0; i < 10; i++){
            bucketList.add(new ArrayList<>());
        }
        for(int i = 0; i < maxDigit; i++, mod *= 10 , div *= 10){
            for(int j = 0; j < array.length; j++){
                //取出各个位数的值
                int num = (array[j] % mod) / div;
                //放入对应的桶中
                bucketList.get(num).add(array[j]);
            }
            int index = 0;
            //将桶内初步排好序的数值逐个放入array数组中
            for(int j = 0; j < bucketList.size(); j++){
                for(int k = 0; k < bucketList.get(j).size(); k++){
                    array[index++] = bucketList.get(j).get(k);
                }
                bucketList.get(j).clear();
            }
        }

        return array;
    }

    public static void main(String[] args) {
        int []arr={2,45,6,12,3,453,6};
        int [] newarr=RadixSort(arr);
        for(int num:newarr){
            System.out.println(num);
        }
    }

}

4、复杂度

时间复杂度:O(k*N) 空间复杂度:O(k + N) 稳定性:稳定

设待排序的数组R[1..n],数组中最大的数是d位数,基数为r(如基数为10,即10进制,最大有10种可能,即最多需要10个桶来映射数组元素)。

处理一位数,需要将数组元素映射到r个桶中,映射完成后还需要收集,相当于遍历数组一遍,最多元素数为n,则时间复杂度为O(n+r)。所以,总的时间复杂度为O(d*(n+r))。

基数排序过程中,用到一个计数器数组,长度为r,还用到一个rn的二位数组来做为桶,所以空间复杂度为O(rn)。

基数排序基于分别排序,分别收集,所以是稳定的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值