学习代码地址,仓库中sortalgorithms工程,感兴趣的可以获取下来看看
仓库地址:https://gitee.com/imdongrui/study-repo.git
摘自百度百科
快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
了解快速排序算法之前,先了解下分治法思想,什么是分治法,分治法就是将一个大的问题,分解成若干个小的问题,然后将这若干个小的问题逐一进行解决,最终得到的结果的总和,就是这个大的问题的处理结果。
通过分治法我们可以将很复杂的问题,解构成一系列简单的问题,再将这些简单问题逐一解决,从而达到解决复杂问题的目的。
快速排序就采用了分治法的思想,我们来看下快速排序的基本思想:
平均时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2)
空间复杂度:O(logn)
是否稳定:不稳定
挖坑填数思想的大神参考链接:https://blog.csdn.net/morewindows/article/details/6684558
不过在操作过程中似乎和大神有些出入,但我的代码经过了一系列验证,应该没有问题。
快速排序的算法思想总体简单,在实现代码时,主要难点集中在如何实现上述的第二点,即将所有比基准数小的数放到其左边,所有比基准数大的数放到其右边,这是排序的核心步骤。
实现的方法有很多种,不过使用最普遍的是挖坑填数的方法,下面详细阐述这个挖坑填数,挖的坑用“#”表示
//需要进行排序的数列
72, 83, 6, 57, 88, 60, 42, 83, 73, 48, 85
//我们选取最左边的数为基准数,即72,可以理解为我们在此处挖了一个坑
#72, 83, 6, 57, 88, 60, 42, 83, 73, 48, 85
//1.从右边开始寻找小于基准数的数来填上面挖的坑,i=0为左边界游标,j=length-1为右边界游标,每次寻找数都从游标开始,游标为每次查找到的数的下标,找到的48用来填了之前的坑,那么48原来的位置就留下了一个坑
//2.基准数最终是放置在i(或者说j,因为最终i=j)的,而48小于基准数,基准数必定在48的右边,所以i+1
//3.此时i=1,j=9
48, 83, 6, 57, 88, 60, 42, 83, 73, #48, 85
//1.从左->右寻找大于等于基准数的数并填坑
//2.基准数最终是放置在j(或者说i,因为最终i=j)的,而83大于等于基准数,基准数必定在83的左边,所以j-1
//3.此时i=1,j=8
48, #83, 6, 57, 88, 60, 42, 83, 73, 83, 85
...
//继续重复上述步骤,即左右交替查找,直到i=j,然后将i位置数赋值为基准数,最终得到结果
48, 42, 6, 57, 60, 72, 88, 83, 73, 83, 85
//此时72已经放在了合理的位置,左边所有的数都小于72,右边所有的数都大于等于72,然后在对72左边和右边的数列进行上述操作,直到所有数列都只有一个元素,排序结束
/**
* 快速排序
*
* @param s 需要排序的数组
* @param l 左边界下标
* @param r 右边界下标
*/
public void quickSort(int[] s, int l, int r) {
if (l < r) {
int i = l, j = r, x = s[l];
while (i < j) {
while (i < j && s[j] >= x)//从右边寻找小于基准数的
j--;
if (i < j)
s[i++] = s[j];
while (i < j && s[i] < x)//从左边寻找大于等于基准数的
i++;
if (i < j)
s[j--] = s[i];
}
s[i] = x;
quickSort(s, l, i - 1);//递归处理左边的数列
quickSort(s, j + 1, r);//递归处理右边的数列
}
}
参考链接:https://www.cnblogs.com/chengxiao/p/6129630.html
部分摘自百度百科
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
既然名字叫堆排序,那我们肯定得先了解下什么是堆。
堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆是非线性数据结构,相当于一维数组,有两个直接后继。
若将和此次序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
既然说堆可以被看做一颗完全二叉树,那么再来看看完全二叉树的定义。
定义:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
由完全二叉树的定义可以得出其有以下性质:
从这些性质可以知道,一棵完全二叉树的元素排列是连续的,则可以将其使用数列来进行表示,例如下图所示:
叨叨半天终于该说堆排序的原理了。
前面介绍堆,我们知道了,一个堆的堆顶总是该堆数据中的最大值(大根堆)或最小值(小根堆),这就是堆排序的关键,利用堆的这个特性,依次将堆中的最大值(或最小值)找出来,最终完成排序。
理解了堆和完全二叉树,那么堆排序的原理也就很清晰了,堆排序主要有以下步骤:
初始化建堆时间复杂度:O(n)
变更元素后重建堆时间复杂度:O(nlogn)
总的时间复杂度:O(nlogn)
空间复杂度:O(1)
在代码实现时,可能会碰到几个疑问点
1 从什么地方开始整理堆?
从最末的一个非叶子节点开始整理,然后依次向前面的节点递进,整理时,用根节点与左右子节点比较,将最大的值赋值给根节点,这样依次传递到根节点,那么根节点(即堆顶)就是整个树中的最大值,正是我们要找的值。
2 如何寻找最末的非叶子节点,以及其左右子节点呢?
最末非叶子节点的下标为x=s.length/2-1
,s为需要整理的堆的数列
其左子节点下标为2*x+1
,右子节点下标为2*x+2
,从完全二叉树的定义可以直到,最末的根节点一定会有左子节点,但不一定有右子节点,所以在处理时需要注意。
/**
* 堆排序
*
* @param s 需要进行排序的数列
* @param end 完全二叉树最后一个元素在数列中的位置下标
*/
void heapSort(int[] s, int end) {
int lastNode = (end + 1) / 2 - 1;//寻找整棵完全二叉树的最后一个非叶子节点
//从找到的last node开始,依次整理二叉树,每次都将二叉树整理为一棵大根堆的完整二叉树,然后将根节点值与最末的节点值交换
for (int i = lastNode; i > -1; i--) {
int l = 2 * i + 1;//左子节点下标
int r = l + 1;//右子节点下标
//根节点与左子节点比较
if (s[i] <= s[l]) {
int tmp = s[i];
s[i] = s[l];
s[l] = tmp;
}
//根节点与右子节点比较
if (r <= end && s[i] <= s[r]) {
int tmp = s[i];
s[i] = s[r];
s[r] = tmp;
}
}
//交换根节点与最末节点值
int tmp = s[0];
s[0] = s[end];
s[end] = tmp;
//递归对剩余的二叉树数列继续进行处理
if (end > 0) heapSort(s, --end);
}
摘自百度百科
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
冒泡排序算法的原理如下:
最好时间复杂度:O(n)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
是否稳定:稳定
/**
* 冒泡排序
*
* @param s 需要进行排序的数列
*/
void bubbleSort(int[] s) {
//对除已经排好序的尾部元素以外的其它元素,进行冒泡查找操作
for (int i = s.length - 1; i > 0; i--) {
//依次比较相邻的两个元素,如果前者大于等于后者,就将它们进行交换
for (int j = 0; j < i; j++) {
if (s[j] >= s[j + 1]) {
int tmp = s[j];
s[j] = s[j + 1];
s[j + 1] = tmp;
}
}
}
}
参考链接:https://blog.csdn.net/llzk_/article/details/51628574
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
直接插入排序适合少量元素的排序。
最好时间复杂度:O(n)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
是否稳定:稳定
/**
* 直接插入排序
* @param s 需要进行排序的数列
*/
void insertionSort(int[] s) {
//从第二个元素开始,将左侧元素作为有序组,右侧元素(含下标为i的元素)作为待插入组
for (int i = 1; i < s.length; i++) {
//取待插入组最左侧元素,即下标为i的元素,依次与有序组比较,如果有序组中的元素大于等于s[i],则进行交换
for (int j = 0; j < i; j++) {
if (s[j] >= s[i]) {
int tmp = s[j];
s[j] = s[i];
s[i] = tmp;
}
}
}
}
部分摘自百度百科
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2
该方法实质上是一种分组插入方法,比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。
是否稳定:不稳定
摘录链接:https://blog.csdn.net/weixin_37818081/article/details/79202115
增量increment的取法有各种方案。
最初shell提出取increment=n/2向下取整,increment=increment/2向下取整,直到increment=1。但由于直到最后一步,在奇数位置的元素才会与偶数位置的元素进行比较,这样使用这个序列的效率会很低。
后来Knuth提出取increment=n/3向下取整+1.还有人提出都取奇数为好,也有人提出increment互质为好。
应用不同的序列会使希尔排序算法的性能有很大的差异。
以下代码选用increment=n/3向下取整+1的增量方式
/**
* 希尔排序
*
* @param s 需要进行排序整理的数列
*/
void shellSort(int[] s) {
int increment = s.length / 3 + 1;
while (true) {
//分组后,对每组数据进行循环排序
for (int i = 0; i < increment; i++) {
//直接插入排序
for (int k = i + increment; k < s.length; k += increment) {
for (int j = i; j <= k - increment; j += increment) {
if (s[j] >= s[k]) {
int tmp = s[j];
s[j] = s[k];
s[k] = tmp;
}
}
}
}
//当增量区间为1时退出循环,排序结束
if (increment == 1) break;
increment = increment / 3 + 1;
}
}
大神的参考链接:https://www.cnblogs.com/chengxiao/p/6194356.html
摘自百度百科,链接:https://baike.baidu.com/item/归并排序/1639015?fr=aladdin
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序主要分为两步:
二路归并原理示意图:
子数列排序的原理示意图,这里直接使用了大神的示意图,非常的清晰明了:
最坏时间复杂度:O(nlogn)
最好时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
是否稳定:稳定
/**
* 归并排序
*
* @param s 需要进行排序的数列
* @param l 数列的左边界,即在s中的下标
* @param r 数列的右边界,即在s中的下标
*/
void mergeSort(int[] s, int l, int r) {
if (l == r) return;//当数列只有一个元素时直接返回
int m = (l + r) / 2;//寻找中间分割线元素位置,数列个数为奇数时,左边序列多取一个元素
mergeSort(s, l, m);//递归处理左边子数列
mergeSort(s, m + 1, r);//递归处理右边子数列
int[] tmp = new int[r - l + 1];
int x = 0, i = l, j = m + 1;
while (x < tmp.length) {
if (i <= m && j <= r) {//当左右子数列游标未走到尽头时,需要比较两边的值
if (s[i] < s[j]) {
tmp[x++] = s[i++];
} else {
tmp[x++] = s[j++];
}
} else {//当左右子数列游标有一个走到尽头时,则不需要比较两边的值
if (i <= m) {
tmp[x++] = s[i++];
}
if (j <= r) {
tmp[x++] = s[j++];
}
}
}
//左右子数列排序完成后,放置回s数列中相应的位置
for (int o = l; o <= r; o++) {
s[o] = tmp[o - l];
}
}
大神的参考链接:https://blog.csdn.net/developer1024/article/details/79770240
摘自百度百科
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
下面这个是大神制作的桶排序的原理图,非常简介生动明了地展示了桶排序的原理:
解析
桶排序其实顾名思义,就是先将所有的元素装进一个个桶子里面(桶子是有序的),然后将桶子里面的数据进行排序(可以采用快排等高效率排序方式),再有序地将桶中的数据依次取出,从而最终得到有序的数据。
通过这种方式,将大规模的数据排序分解成很多个小规模的数据排序,避免数据规模过大导致的排序算法性能急剧下降。
桶函数
个人认为,桶排序的难点主要在于桶函数的设计,桶应该是一组连续的数值区间,不能造成数据遗漏,桶函数就是用来将数据准确地放置进桶中的函数f(k)。桶函数应该保证效率的同时尽可能地让所有元素均匀地分布在各个桶中,这点就比较难实现了,因为数据的分布情况是千差万别的,感觉很难有一种普适性的算法适用于所有情况,可能针对特定的情况,进行特定的优化更好。一个桶排序的性能好与坏,桶函数的设计有至关重要的影响。
/**
* 桶排序
*
* @param s 需要进行排序的数列
*/
void bucketSort(int[] s) {
Map<Integer, List<Integer>> bucketMap = new HashMap<>();
//将所有元素依次放置进对应的桶中
for (int e : s) {
//此处的桶函数为f(k)=x/10,简单地将跨度10作为区间,不过在正负数交界处,(-10,10)的跨度为20
//这种方法适用于在10的跨度上分布比较均匀的数值,如果过分集中于某一个或某几个区间,则性能提升不大
int no = e / 10;
if (!bucketMap.containsKey(no)) {
bucketMap.put(no, new ArrayList<>());
}
bucketMap.get(no).add(e);
}
//将key排序
List<Integer> keyList = new ArrayList<>(bucketMap.keySet());
keyList.sort(Comparator.comparingInt(x -> x));
//将桶中的元素依次取出,放置回s数列中
int i = 0;
for (Integer key : keyList) {
List<Integer> list = bucketMap.get(key);
list.sort(Comparator.comparingInt(x -> x));
for (Integer num : list) {
s[i++] = num;
}
}
}
摘自360百科
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
最坏时间复杂度:O(n^2)
最好时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
是否稳定:不稳定
/**
* 选择排序
*
* @param s 需要进行排序的数列
*/
void selectionSort(int[] s) {
//依次将数列中的数作为基数和右边的数进行比较,找到最小的
for (int i = 0; i < s.length; i++) {
for (int j = i; j < s.length; j++) {
if (s[i] > s[j]) {
int tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}
}
}
解析
这段代码看起来和直接插入排序差不多,但是其实完全不一样的,仔细看,它们的i和j的初始值不同,这代码中细微的差别,就体现了直接插入排序和选择排序不同的算法思想。
大神参考链接:https://blog.csdn.net/qq_34801169/article/details/81412340
摘自360百科
计数排序是一个非基于比较的排序算法,该算法于1954年由Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。
当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)),如归并排序,堆排序)
无耻地盗用大神的原理演示图:
解析
这个原理图已经很清楚地展示了计数排序的算法思想。
说白了就是对各个元素放到一个桶里并记数,然后将桶进行排序,再然后按照计数依次将数值填回数列就是排序后的数列了。
计数排序比较适合用于有很多重复元素的数列排序。
最坏时间复杂度:O(n+k)
最好时间复杂度:O((n+k)
平均时间复杂度:O((n+k)
是否稳定:稳定
这里使用了hashmap做桶,应该还有更好的实现方式。
/**
* 计数排序
*
* @param s 需要排序的数列
*/
void countingSort(int[] s) {
Map<Integer, Integer> countingMap = new HashMap<>();
//循环s中每个元素,并计数
for (int i : s) {
countingMap.put(i, countingMap.containsKey(i) ? countingMap.get(i) + 1 : 1);
}
//将key排序,即使计数桶有序
List<Integer> keyList = new ArrayList<>(countingMap.keySet());
keyList.sort(Comparator.comparingInt(x -> x));
//按照计数将元素依次放回数列中
int i = 0;
for (Integer key : keyList) {
for (int j = 0, limit = countingMap.get(key); j < limit; j++) {
s[i++] = key;
}
}
}
一如既往的大神链接:https://blog.csdn.net/double_happiness/article/details/72452243
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
还是上大神做的原理图吧,清楚明了
基数排序通常比较适用于整数的排序,其原理就是依次按照各数位上的数进行排序,每次排序和桶排序类似,都有一个入桶和出桶的过程,经过所有的数位排序后,最终得到一个有序数列。
从低位向高位依次处理的叫做LSD
从高位向低位依次处理的叫做MSD
当高位的数居多时,MSD的性能较好
最坏时间复杂度:O(n*k)
最好时间复杂度:O(n*k)
平均时间复杂度:O(n*k)
是否稳定:稳定
/**
* 基数排序
*
* @param s 需要排序的数列
*/
void radixSortLSD(int[] s) {
//寻找最大的数,并以此确定需要循环的次数loop,即max的位数
int max = Integer.MIN_VALUE, loop;
for (int e : s) {
if (e > max)
max = e;
}
loop = (max + "").length();
Map<Integer, List<Integer>> bucketMap = new HashMap<>();
for (int i = 0; i < loop; i++) {
//根据i计算digit,digit用于计算特定位上的数
int digit = 1;
for (int x = 0; x < i; x++) {
digit *= 10;
}
//start和end用于后面依次从桶中取出数据,此处start与end,后面就不再对keys排序,但有一定局限性,例如位数比较分散时要做很多无用功
int start = Integer.MAX_VALUE, end = Integer.MIN_VALUE;
//根据特定位的数将元素放入指定的桶中
for (int e : s) {
int key = e % (digit * 10) / digit;
if (key < start) start = key;
if (key > end) end = key;
if (!bucketMap.containsKey(key))
bucketMap.put(key, new ArrayList<>());
bucketMap.get(key).add(e);
}
//从桶中依次将元素取出放置回s中
int index = 0;
for (int j = start; j <= end; j++) {
if (bucketMap.containsKey(j)) {
for (Integer e : bucketMap.get(j)) {
s[index++] = e;
}
}
}
bucketMap.clear();//每次处理后将映射桶清空
}
}