排序算法
- 排序算法的介绍:
- 排序也称排序算法(sort Alogrithm),排序时将以组数据,依指定顺序进行排序的过程。
- 排序的分类:
- 内部排序:
- 指将需要处理的所有数据加载到内部存储器中进行排序。
- 外部排序:
- 数据量过大,无法加载到内存中,需要借助外部存储进行排序。
- 常见的排序算法分类:
-
排序算法的复杂度和稳定性:
- 内部排序:
冒泡排序
-
基本介绍:
-
冒泡排序(Bubble Sorting) 的基本思想:
-
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使较大相邻的元素从前向后移动,就像水下的气泡一样逐渐升起来。
-
因排序的过程中,各元素不断接近自己的位置,**如果下 一趟比较下来没有进行交换,就说明序列有序,因此要在排序过程中设置一个标志的flag判断元素是否进行过交换。从而减少不比较的比较。
-
冒泡图解:
-
-
将列表[1,3,5,4,2]进行排序,把元素想象成一个小球。如下图所示:
第一次循环进行交换最大值5浮到顶部。接下来进行第二次循环: 5在第一次已经冒出来,第二次循环不需要进行交换比较
第三次循环[4,5]已经冒出来,第三次循环不需要进行交换比较
第四次循环:到第4次循环结束之后,全部元素交换完成,得到一个有序的列表。
-
-
来源:图解冒泡来自
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)
-
代码实现:
-
public class BubbleSort { public static void main(String[] args) { int[] a = {5, 4, 3, 2, 1}; for (int i = 0; i < 5 - 1; ++ i) { boolean flag = true; for (int j = 0; j < 5 - i - 1; ++ j) { if (a[j] > a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; flag = false; } } for (int j = 0; j < 5; ++ j) { System.out.print(a[j] + " "); } System.out.println(); if (flag) break; } } }
-
-
冒泡排序小结:
-
- 一共进行数组大小 - 1次 的循环
- 每一趟排序的次数在逐渐减少。
- 如果我们发现在某一趟排序中,没有发生依次交换,可以提前结束冒泡排序。这就是一个优化,flag 变量做这一步。
-
-
-
-
选择排序
-
基本思想:
- 选择排序(select sorting)也是一种简单的排序方法。它的 基本思想 是:第一次从arr[0] ~ arr[n-1]中选这最小的值,与arr[0]交换,第二次从arr[1]arr[n-1]中选择最小的值,与arr[1]交换,第i次从arr[i-1]arr[n-1]中选取最小值,与arr[i-1]交换。。。。. 第n-1次 从arr[n-2]~arr[n-1]选最小的值,与arr[n-2]交换,总共通过 n - 1次,的到一个安排序序码从小到大大排序的有序序列。
-
图解:
-
代码思路:
-
- 选择排序一共数组大小 -1 轮排序。
- 每 1 轮排序,又是一个循环,循环规则如下
-
- 先假定当前这个数是最小的。
- 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小的数,并的到下标。
- 当遍历到数组最后时,本轮得到本轮最小的数的下标。
- 交换(看代码)
-
-
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)
-
代码实现:
-
public class SelectSort { public static void main(String[] args) { int[] arr = {8, 3, 2, 1, 7, 4, 6, 5}; for (int i = 0; i < 8; ++ i) { int idx = i; for (int j = i; j < 8; ++ j) { if (arr[idx] > arr[j]) { idx = j; } } int temp = arr[idx]; arr[idx] = arr[i]; arr[i] = temp; for (int j = 0; j < 8; ++ j) { System.out.print(arr[j] + " "); } System.out.println(); } } }
-
插入排序
-
插入排序介绍:
- 插入式排序属于内部排序法,对于欲排序的元素以插入的方式找到该元素的适当位置,以达到排序的目的。
-
插入排序的思想:
- 把n个待排序的元素看成为,一个有序和一个无序表,开始时有序表中只包含一个元素,无序表中包含了n-1个元素, 排序过程中每次从无序表中取出第一个元素,把它的排序码一次与有序表元素的排序码进行比较,将它插入到有序表中的适当位子,使之称为新的有序表。
- 就像一手扑克牌整理成有序的。
-
图解:
-
代码实现:
-
public class InsertSort { public static void main(String[] args) { int[] arr = {17, 3, 25, 14, 20, 9}; for (int i = 1; i < arr.length; ++ i) { int insertValue = arr[i]; int insertIndex = i - 1;//表示有序表的最后这个元素的下标 //还没找到位置 while (insertIndex >= 0 && insertValue < arr[insertIndex]) { arr[insertIndex + 1] = arr[insertIndex]; insertIndex -= 1; } //退出while循环 说明插入的位置被找到;额 arr[insertIndex + 1] = insertValue; for (int j = 0; j <= i; ++ j) { System.out.print(arr[j] + " "); } System.out.println(); } } }
-
运行效果图:
-
希尔排序
是将简单的插入排序进行优化。
-
希尔排序的介绍:
- 希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单排序经过改良后的一个 更高效的版本,也被称为缩小增量排序。
-
希尔排序的算法思想:
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量的减少,每组包含的关键词越来越多,当增量减到 1 时,整个文件恰好被分成一组,使算法停止。
-
图解希尔排序:
-
原始数组 以下数据元素颜色相同为一组
-
初始增量 g a p = l e n g t h / 2 = 5 gap=length/2=5 gap=length/2=5 ,意味着真个数组被分为 5 5 5 组, [ 8 , 3 ] [ 9 , 5 ] [ 1 , 4 ] [ 7 , 6 ] [ 2 , 0 ] [8,3][9,5][1,4][7,6][2,0] [8,3][9,5][1,4][7,6][2,0]
-
此状态到下一个状态的代码:
-
for (int i = 5; i < arr.length; ++ i) { for (int j = i - 5; j >= 0; j -= 5) { //如果当前元素大于加上步长的元素,说明交换 if (arr[j] > arr[j + 5]) { int temp = arr[j]; arr[j] = arr[j + 5]; arr[j + 5] = temp; } } } System.out.println("希尔排序第一轮"+Arrays.toString(arr));
-
-
对这五组分别进行直接 插入排序, 结果如下,可以看到,像3, 5,6这些小的元素都被掉到了最前面了,然后缩小增量 g a p = 5 / 2 = 2 gap=5/2=2 gap=5/2=2,数组被分为 2 组 [ 3 , 1 , 0 , 9 , 7 ] [ 5 , 6 , 8 , 4 , 2 ] [3,1,0,9,7][5,6,8,4,2] [3,1,0,9,7][5,6,8,4,2]
-
此状态到下一个状态的代码:
-
for (int i = 2; i < arr.length; ++ i) { for (int j = i - 2; j >= 0; j -= 2) { //如果当前元素大于加上步长的元素,说明交换 if (arr[j] > arr[j + 2]) { int temp = arr[j]; arr[j] = arr[j + 2]; arr[j + 2] = temp; } } } System.out.println("希尔排序第二轮"+Arrays.toString(arr));
-
-
对以上 2 组再分别进行直接插入排序,结果如下,可以看到,此时整个数组的有序程度更异步接近啦。再缩小增量 g a p = 2 / 2 = 1 gap=2/2=1 gap=2/2=1,此时,整个数组为 1 组 [ 0 , 2 , 1 , 4 , 3 , 5 , 7 , 6 , 9 , 8 ] [0,2,1,4,3,5,7,6,9,8] [0,2,1,4,3,5,7,6,9,8]
-
此状态到有序的代码:
-
for (int i = 1; i < arr.length; ++ i) { for (int j = i - 1; j >= 0; j -= 1) { //如果当前元素大于加上步长的元素,说明交换 if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } System.out.println("希尔排序第三轮"+Arrays.toString(arr));
-
-
经过上面的”宏观调控“,整个数组的无序化程度成果喜人。此时,仅仅需要对以上数列简单微调,无需大量移动即可完成整个数组的排序。
-
-
时间复杂度: O ( n k ) O(n^k) O(nk) 1.3 <= k <= 2.
-
将上面的逐步分析使用循环处理:
-
import java.util.Arrays; public class ShellSort { public static void main(String[] args) { int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; shellSort(arr); } //使用希尔排序的方式来编写希尔排序 public static void shellSort(int arr[]) { for (int gap = arr.length / 2; gap > 0; gap /= 2) { for (int i = gap; i < arr.length; ++ i) { for (int j = i - gap; j >= 0; j -= gap) { if (arr[j] > arr[j + gap]) { int temp = arr[j]; arr[j] = arr[j + gap]; arr[j + gap] = temp; } } } System.out.println(Arrays.toString(arr)); } } }
-
js版
// 基数排序
function radixSort(arr = []) {
// 1. 获取数组中的最大值
const maxVal = Math.max.apply(Math, arr);
// 2. 计算最大长度
const maxLen = String(maxVal).length;
const n = arr.length;
// 3. 定义一个基数桶
const bucket = Array.from({ length: 10 }, () => new Array(n).fill(-1));
const bucketCount = new Array(10).fill(0);
// 4. 开始奇数排序,从个位开始
for (let i = 0, base = 1; i < maxLen; ++i, base *= 10) {
// 将数加入到桶中
for (let j = 0; j < n; ++j) {
const u = Math.floor(arr[j] / base) % 10;
bucket[u][bucketCount[u]++] = arr[j];
}
let idx = 0;
// 从桶中取出数据并放回原来的数组中
for (let j = 0; j < 10; ++j) {
for (let k = 0; k < bucketCount[j]; ++k) {
(arr[idx++] = bucket[j][k]), (bucket[j][k] = -1);
}
bucketCount[j] = 0;
}
}
}
const arr = [53, 3, 542, 748, 14, 214, 111, 1, 2];
radixSort(arr);
console.log(arr);
快速排序
-
快速排序算法介绍:
-
快速排序(Quicksort)是对冒泡排序的一种改进。
-
基本思想:
- 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的左右数据都比另外一部的所有数据都要小,然后再按此方法对着两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
-
-
做法:
- 选择每个子数组中间的一个数作为 基准数,将基准数的右边变成全部大于 基准数的数, 左边全部小于基准数的数。然后再递归处理子数组。
- 图解此图是以左边的数,以中间的数更好理解:
-
时间复杂度: O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
-
代码实现:
-
import java.util.Arrays; public class QuickSort { public static void main(String[] args) { int[] a = {2, 3, 1, 2, 5, 21, 1, 0}; quickSort(a, 0, a.length - 1); System.out.println(Arrays.toString(a)); } public static void quickSort(int a[], int l, int r) { if (l >= r) return; int i = l - 1; int j = r + 1; int x = a[(l + r) >> 1]; while (i < j) { do i ++; while (a[i] < x); do j --; while (a[j] > x); if (i < j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } } quickSort(a, l, j); quickSort(a, j + 1, r); } }
-
输出结果
-
归并排序
-
归并排序介绍:
-
归并排序(MERGE-SORT)是利用 归并 的思想实现的排序方法,该算法采用经典的 分治 (divide-and-conquer)策略(分治法将问题 分 成一些小问题然后递归求解,而 治 的阶段则是将各分段得到的各答案”修补“在一起,即 分而治之)。
-
图解算法思想:
- 说明:
- 可以看到这种结构很像一颗完全二叉树,本文的归并排序我们采用递归去实现(也可以采用迭代的方法实现)。
- 分 阶段可以理解为就是递归拆分子序列的过程。
- 治 阶段把原本有序的两个序列 合并成一个有序序列。
- 说明:
-
-
归并排序代码实现:
-
import java.util.Arrays; public class MergeSort { public static void main(String[] args) { int[] a = {8, 4, 5, 7, 1, 3, 6, 2}; int[] c = new int[8]; mergeSort(a, 0, a.length - 1, c); System.out.println(Arrays.toString(a)); } public static void mergeSort(int[] a, int l, int r, int[] c) { if (l >= r) return; int mid = (l + r) >> 1; mergeSort(a, l, mid, c); mergeSort(a, mid + 1, r, c); int i = l; int j = mid + 1; for (int k = l; k <= r; ++ k) { if (j > r || i <= mid && a[i] <= a[j]) c[k] = a[i ++]; else c[k] = a[j ++]; } for (int k = l; k <= r; ++ k) a[k] = c[k]; } }
-
运行结果:
-
基数排序
-
基数排序(桶排序)介绍:
-
- 基数排序(radix sort)属于"分配式排序",又称 “桶子法” (bucket sort)或者bin sort,,顾名思义,它是通过键值的各个位的值,将要排序的元素分配到某些 桶 中,达到排序的效果。
- 基数排序法是属于稳定的排序,基数排序法的效率高的稳定性排序算法。
- 基数排序是(Radix Sort)是桶排序的扩展。
- 基数排序是1887年 赫尔曼·何乐礼 发明的。它的实现:将整数按位数切割成不同的数字,然后按每个数字分别进行比较。
-
-
基数排序的基本思想:
-
-
将所有待比较的数值统一为一样长度的数,位数较短的数前面补零。然后,从低位开始,依次进行依次排序。这样从低位排序一直到高位排序完成以后,数列就变成了一个有序序列。
-
这样说明,比较难理解,下面图文理解。
-
数组的初始状态:{53, 3, 542,748,14,214}使用基数排序,进行升序排序。
-
下面是10个桶(表示 0~9 ):
-
0 1 2 3 4 5 6 7 8 9
-
-
-
将每个元素的个位取出来,然后看看这个数应该放在那个对应的桶里(以最取出来的数选桶)。
*
0 1 2 3 4 5 6 7 8 9
-
按照这个桶的顺序依次,取出数据放回到原来的数组中。
- {542,53,3,14,214,748}
- 此时的桶 中的元素还在。
-
将每个元素的十位取出来,然后看看这个数应该放在那个对应的桶里(以最取出来的数选桶)。
-
为了方便先把之气的元素隐藏,但是之前放的元素还再桶里。
-
0 1 2 3 4 5 6 7 8 9
-
-
按照桶的顺序取出数据放回原来的数组中
- {3,14,214,542,748,53}
-
将每个元素的百位取出来,然后看看这个数应该放在那个对应的桶里(以最取出来的数选桶)。
- 最后 同理取出来得到数组 {3,14,214,542,748,53}
-
-
package com.atguigu.sort; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; public class RadixSort { public static void main(String[] args) { int arr[] = { 53, 3, 542, 748, 14, 214}; // 80000000 * 11 * 4 / 1024 / 1024 / 1024 =3.3G // int[] arr = new int[8000000]; // for (int i = 0; i < 8000000; i++) { // arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数 // } System.out.println("排序前"); Date data1 = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String date1Str = simpleDateFormat.format(data1); System.out.println("排序前的时间是=" + date1Str); radixSort(arr); Date data2 = new Date(); String date2Str = simpleDateFormat.format(data2); System.out.println("排序前的时间是=" + date2Str); System.out.println("基数排序后 " + Arrays.toString(arr)); } //基数排序方法 public static void radixSort(int[] arr) { //根据前面的推导过程,我们可以得到最终的基数排序代码 //1. 得到数组中最大的数的位数 int max = arr[0]; //假设第一数就是最大数 for(int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } //得到最大数是几位数 int maxLength = (max + "").length(); //定义一个二维数组,表示10个桶, 每个桶就是一个一维数组 //说明 //1. 二维数组包含10个一维数组 //2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length //3. 名明确,基数排序是使用空间换时间的经典算法 int[][] bucket = new int[10][arr.length]; //为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数 //可以这里理解 //比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数 int[] bucketElementCounts = new int[10]; //这里我们使用循环将代码处理 for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) { //(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位.. for(int j = 0; j < arr.length; j++) { //取出每个元素的对应位的值 int digitOfElement = arr[j] / n % 10; //放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组) int index = 0; //遍历每一桶,并将桶中是数据,放入到原数组 for(int k = 0; k < bucketElementCounts.length; k++) { //如果桶中,有数据,我们才放入到原数组 if(bucketElementCounts[k] != 0) { //循环该桶即第k个桶(即第k个一维数组), 放入 for(int l = 0; l < bucketElementCounts[k]; l++) { //取出元素放入到arr arr[index++] = bucket[k][l]; } } //第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!! bucketElementCounts[k] = 0; } //System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr)); } /* //第1轮(针对每个元素的个位进行排序处理) for(int j = 0; j < arr.length; j++) { //取出每个元素的个位的值 int digitOfElement = arr[j] / 1 % 10; //放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组) int index = 0; //遍历每一桶,并将桶中是数据,放入到原数组 for(int k = 0; k < bucketElementCounts.length; k++) { //如果桶中,有数据,我们才放入到原数组 if(bucketElementCounts[k] != 0) { //循环该桶即第k个桶(即第k个一维数组), 放入 for(int l = 0; l < bucketElementCounts[k]; l++) { //取出元素放入到arr arr[index++] = bucket[k][l]; } } //第l轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!! bucketElementCounts[k] = 0; } System.out.println("第1轮,对个位的排序处理 arr =" + Arrays.toString(arr)); //========================================== //第2轮(针对每个元素的十位进行排序处理) for (int j = 0; j < arr.length; j++) { // 取出每个元素的十位的值 int digitOfElement = arr[j] / 10 % 10; //748 / 10 => 74 % 10 => 4 // 放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } // 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组) index = 0; // 遍历每一桶,并将桶中是数据,放入到原数组 for (int k = 0; k < bucketElementCounts.length; k++) { // 如果桶中,有数据,我们才放入到原数组 if (bucketElementCounts[k] != 0) { // 循环该桶即第k个桶(即第k个一维数组), 放入 for (int l = 0; l < bucketElementCounts[k]; l++) { // 取出元素放入到arr arr[index++] = bucket[k][l]; } } //第2轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!! bucketElementCounts[k] = 0; } System.out.println("第2轮,对个位的排序处理 arr =" + Arrays.toString(arr)); //第3轮(针对每个元素的百位进行排序处理) for (int j = 0; j < arr.length; j++) { // 取出每个元素的百位的值 int digitOfElement = arr[j] / 100 % 10; // 748 / 100 => 7 % 10 = 7 // 放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } // 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组) index = 0; // 遍历每一桶,并将桶中是数据,放入到原数组 for (int k = 0; k < bucketElementCounts.length; k++) { // 如果桶中,有数据,我们才放入到原数组 if (bucketElementCounts[k] != 0) { // 循环该桶即第k个桶(即第k个一维数组), 放入 for (int l = 0; l < bucketElementCounts[k]; l++) { // 取出元素放入到arr arr[index++] = bucket[k][l]; } } //第3轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!! bucketElementCounts[k] = 0; } System.out.println("第3轮,对个位的排序处理 arr =" + Arrays.toString(arr)); */ } }
-
-
-
堆排序
-
-
堆排序是利用 堆 这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好,平均时间复杂度均为 O ( l o n g n ) O(long^n) O(longn),它也是不稳定排序
-
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,**注意:**没有要求结点的左孩子和右孩子的值的关系大小。
-
每个结点的值都小于或者等于其左右孩子结点的值,称为小顶堆。
-
大顶堆举例说明
-
小顶堆:
-
arr[i] <= arr[2 * i + 1] && arr[i] <= arr[2 * i + 2] //i对应的第几个结点,i从0开始编号
-
-
一般 升序采用大顶堆 ,降序采用小顶堆。
-
堆排序思想:
- 将待排序序列构造成一个大顶堆。
- 此时整个序列的最大值就是堆顶的根结点.
- 将其与末尾元素进行交换,此时末尾就成为最大值。
- 将剩余 n - 1 个元素重新构一个堆,这样就会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了···
package com.yanxun.Tree;
import java.lang.reflect.Array;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] a = {4, 6, 8, 5, 9};
heapSort(a);
System.out.println(Arrays.toString(a));
}
public static void heapSort(int[] arr) {
System.out.println("堆排序");
for (int i = arr.length/2 - 1; i >= 0; i --) {
adjustHeap(arr, i, arr.length);
}
for (int j = arr.length - 1; j > 0; -- j) {
int temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
}
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];//取出当前元素的值
//调整
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) {//比较i的左右结点大小
k ++;
}
if (arr[k] > temp) {
arr[i] = arr[k];//把较大的值向上调整
i = k;
} else {
break;
}
}
arr[i] = temp;
}
}