排序算法动画演示
本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net
一、直接插入排序 (Straight Insertion Sorting)
把新的数据插入到已经排好的数据列中。将第一个数和第二个数排序,然后构成一个有序序列;将第三个数插入进去,构成一个新的有序序列;对第四个数、第五个数…… 直到最后一个数,重复第二步。如图:
1️⃣基本思想:在要排序的一组数中,假设前面 (n-1) [n>=2] 个数已经是排好顺序的,现在要把第 n 个数插到前面的有序数中,使得这 n 个数也是排好顺序的。如此反复循环,直到全部排好顺序。
2️⃣代码实现:首先设定插入次数,即循环次数,for(int i=1;i<length;i++)
,1 个数的那次不用插入。设定要插入的数和得到已经排好序列的最后一个数的位数:insertNum
和j=i-1
。从最后一个数开始向前循环,如果插入数小于当前数,就将当前数向后移动一位。将当前数放置到空着的位置,即 j+1。代码如下:
public void insertSort(int[] a) {
int len = a.length;//单独把数组长度拿出来,提高效率
int insertNum;//要插入的数
for (int i = 1; i < len; i++) {//因为第一次不用,所以从1开始
insertNum = a[i];
int j = i - 1;//序列元素个数
while (j >= 0 && a[j] > insertNum) {//从后往前循环,将大于insertNum的数向后动
a[j + 1] = a[j];//元素向后移动
j--;
}
a[j + 1] = insertNum;//找到位置,插入当前元素
}
}
二、希尔排序
针对直接插入排序的效率问题,有人对此进行了改进与升级,这就是现在的希尔排序。希尔排序
又称递减增量排序算法
,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
如图:
对于直接插入排序问题,数据量巨大时。将数的个数设为 n,取奇数 k=n/2,将下标差值为 k 的数分为一组,构成有序序列。再取 k=k/2 ,将下标差值为 k 的书分为一组,构成有序序列。重复第二步,直到 k=1 执行简单插入排序。代码如下:
首先确定分的组数。然后对组中元素进行插入排序。然后将 length/2,重复 1,2 步,直到 length=0 为止。
public void sheelsort(int[] a) {
int len = a.length; //单独把数组长度拿出来,提高效率
while (len != 0) {
len = len / 2;
for (int i = 0; i < len; i++) {//分组
for (int j = i + len; j < a.length; j += len) { //元素从第二个开始
int k = j - len;// k为有序序列最后一位的位数
int temp = a[j];//要插入的元素
while (k >= 0 && temp < a[k]) {// /从后往前遍历
a[k + len] = a[k];
k -= len;//向后移动len位
}
a[k + len] = temp;
}
}
}
}
三、简单选择排序
常用于取序列中最大最小的几个数时。如果每次比较都交换,那么就是交换排序;如果每次比较完一个循环再交换,就是简单选择排序。
- 遍历整个序列,将最小的数放在最前面。
- 遍历剩下的序列,将最小的数放在最前面。
- 重复第二步,直到只剩下一个数。
代码实现:
- 首先确定循环次数,并且记住当前数字和当前位置。
- 将当前位置后面所有的数与当前数字进行对比,小数赋值给 key,并记住小数的位置。
- 比对完成后,将最小的值与第一个数的值交换。
- 重复 2、3 步。
public void selectSort(int[] a) {
int len = a.length;
for (int i = 0; i < len; i++) {// 循环次数
int value = a[i];
int position = i;
for (int j = i + 1; j < len; j++) {//找到最小的值和位置
if (a[j] < value) {
value = a[j];
position = j;
}
}
a[position] = a[i];//进行交换
a[i] = value;
}
}
四、堆排序
对简单选择排序的优化:
- 将序列构建成大顶堆。
- 将根节点与最后一个节点交换,然后断开最后一个节点。
- 重复第一、二步,直到所有节点断开。
代码如下:
public void heapSort(int[] a) {
int len = a.length;//循环建堆
for (int i = 0; i < len - 1; i++) {//建堆
buildMaxHeap(a, len - 1 - i);//交换堆顶和最后一个元素
swap(a, 0, len - 1 - i);
}
}
//交换方法
private void swap(int[] data, int i, int j) {
int tmp = data[i];
data[i] = data[j];
data[j] = tmp;
}
//对data数组从0到lastIndex建大顶堆
private void buildMaxHeap(int[] data, int lastIndex) {
//从lastIndex处节点(最后一个节点)的父节点开始
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
//k保存正在判断的节点
int k = i;
//如果当前k节点的子节点存在
while (k * 2 + 1 <= lastIndex) {
//k节点的左子节点的索引
int biggerIndex = 2 * k + 1;
//如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在
if (biggerIndex < lastIndex) {
//若果右子节点的值较大
if (data[biggerIndex] < data[biggerIndex + 1]) {
//biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
//如果k节点的值小于其较大的子节点的值
if (data[k] < data[biggerIndex]) {
//交换它们
swap(data, k, biggerIndex);
//将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
k = biggerIndex;
} else {
break;
}
}
}
}
五、冒泡排序
- 将序列中所有元素两两比较,将最大的放在最后面。
- 将剩余序列中所有元素两两比较,将最大的放在最后面。
- 重复第二步,直到只剩下一个数。
代码实现:
- 设置循环次数。
- 设置开始比较的位数,和结束的位数。
- 两两比较,将最小的放到前面去。
- 重复 2、3 步,直到循环次数完毕。
public static void bubbleSort(int[] arr) {
for(int i =0;i<arr.length-1;i++) {
for(int j=0;j<arr.length-i-1;j++) {//-1为了防止溢出
if(arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
六、快速排序
要求时间最快时。
选择第一个数为 p,小于 p 的数放在左边,大于 p 的数放在右边。递归的将 p 左边和右边的数都按照第一步进行,直到不能递归。
代码实现
public static void quickSort(int[] a, int start, int end) {
if (start < end) {
int baseNum = a[start];//选基准值
int i = start;
int j = end;
/**
* 循环处理后:
* 1.baseNum左边的数均为比其小的数
* 2.baseNum右边的数均为比其大的数
* 3.直到 i>=j,执行后续递归逻辑
*/
while (i < j) {
/**
* 跳出while的情况:
* 1.a[i]>=baseNum
* 2.i==end【不考虑,因为起始为{0,9}】
*/
while ((a[i] < baseNum) && i < end) {
++i;
}
/**
* 跳出while的情况:
* 1.a[j]<=baseNum
* 2.j==start【不考虑,因为起始为{0,9}】
*/
while ((a[j] > baseNum) && j > start) {
--j;
}
/**
* 到此时:
* 1.a[i]>=baseNum
* 2.a[j]<=baseNum
* 3.交换a[i]与a[j]的值
* 4.挪动左右指针,比较下一轮
*/
if (i <= j) {
int midNum = a[j];
a[j] = a[i];
a[i] = midNum;
++i;
--j;
}
for (int ii = 0; ii < a.length; ii++) {
System.out.print(a[ii] + " ");
}
System.out.println("……………………");
}
/**
* 递归左半部分快排
*/
if (start < j) {
quickSort(a, start, j);
}
/**
* 递归右半部分快排
*/
if (end > i) {
quickSort(a, i, end);
}
}
}
七、归并排序
速度仅次于快速排序,内存少的时候使用,可以进行并行计算的时候使用。
- 选择相邻两个数组成一个有序序列。
- 选择相邻的两个有序序列组成一个有序序列。
- 重复第二步,直到全部组成一个有序序列。
public void mergeSort(int[] a, int left, int right) {
int t = 1;// 每组元素个数
int size = right - left + 1;
while (t < size) {
int s = t;// 本次循环每组元素个数
t = 2 * s;
int i = left;
while (i + (t - 1) < size) {
merge(a, i, i + (s - 1), i + (t - 1));
i += t;
}
if (i + (s - 1) < right)
merge(a, i, i + (s - 1), right);
}
}
private static void merge(int[] data, int p, int q, int r) {
int[] B = new int[data.length];
int s = p;
int t = q + 1;
int k = p;
while (s <= q && t <= r) {
if (data[s] <= data[t]) {
B[k] = data[s];
s++;
} else {
B[k] = data[t];
t++;
}
k++;
}
if (s == q + 1) {
B[k++] = data[t++];
} else {
B[k++] = data[s++];
}
for (int i = p; i <= r; i++)
data[i] = B[i];
}
八、基数排序
用于大量数,很长的数进行排序时。
将所有的数的个位数取出,按照个位数进行排序,构成一个序列。将新构成的所有的数的十位数取出,按照十位数进行排序,构成一个序列。代码如下:
public void baseSort(int[] a) {
//首先确定排序的趟数;
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
int time = 0;
//判断位数;
while (max > 0) {
max /= 10;
time++;
}
//建立10个队列;
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
//进行time次分配和收集;
for (int i = 0; i < time; i++) {
//分配数组元素;
for (int j = 0; j < a.length; j++) {
//得到数字的第time+1位数;
int x = a[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x, queue2);
}
int count = 0;//元素计数器;
//收集队列元素;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
ArrayList<Integer> queue3 = queue.get(k);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
新建测试类进行测试
public class TestSort {
public static void main(String[] args) {
int []a=new int[10];
for(int i=1;i<a.length;i++){
//a[i]=(int)(new Random().nextInt(100));
a[i]=(int)(Math.random()*100);
}
System.out.println("排序前的数组为:"+ Arrays.toString(a));
Sort s=new Sort();
//排序方法测试
//s.insertSort(a);
//s.sheelSort(a);
//s.selectSort(a);
//s.heapSort(a);
//s.bubbleSort(a);
//s.quickSort(a, 1, 9);
//s.mergeSort(a, 3, 7);
s.baseSort(a);
System.out.println("排序后的数组为:"+Arrays.toString(a));
}
}
部分结果如下:
九、计数排序 (Counting Sort)
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序 (Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组 C,其中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置。它只能对整数进行排序。
算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
- 对所有的计数累加 (从 C 中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1。
代码实现:
public static int[] CountintSort(int[] array) {
if (array.length == 0) {
return array;
}
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else {
i++;
}
}
return array;
}
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 C 的长度取决于待排序数组中数据的范围 (等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)
十、桶排序 (Bucket Sort)用空间换时间
1️⃣思想介绍【百度百科】
桶排序或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序 (有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间 Θ(n)。但桶排序并不是比较排序,它不受到 O(n log n) 下限的影响。
简单理解:
- 将待排序的序列分到若干个桶中,每个桶内的元素再进行个别排序。
- 时间复杂度最好可能是线性 O(n),桶排序不是基于比较的排序。
2️⃣桶排序借助桶的位置完成一次初步的排序——将待排序元素分别放至各个桶内。通常根据待排序元素整除的方法将其较为均匀的放至桶中,如8 5 22 15 28 9 45 42 39 19 27 47 12
这个待排序序列,假设放入桶编号的规则为:n/10。这样首先各个元素就可以直接通过整除的方法放至对应桶中。而右侧所有桶内数据都比左侧的要大!
在刚刚放入桶中的时候,各个桶的大小相对可以确定,右侧都比左侧大,但桶内还是无序的,对各个桶内分别进行排序,再依次按照桶的顺序、桶内序列顺序得到一个最终排序的序列。
3️⃣具体实现思路
用 List[] 表示桶,每个 List 代表一个桶,将数据根据整除得到的值直接放到对应编号的集合里面去,再依次进行排序就可以了。
- 人为设置一个 BucketSize,作为每个桶所能放置多少个不同数值 (例如当
BucketSize==5
时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放 100 个 3)。 - 遍历输入数据,并且把数据一个一个放到对应的桶里去。
- 对每个非空桶进行排序,可以使用其它排序方法,也可以递归使用桶排序。
- 从非空桶里把排好序的数据拼接起来。
注意,如果递归使用桶排序为各个桶排序,则当桶数量为 1 时要手动减小 BucketSize 增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。
4️⃣桶排序算法分析
桶排序有很多不一样的地方,无论是算法时间复杂度还是整个算法的流程。
桶排序的时间复杂度到底是多少?桶排序的算法时间复杂度由两部分组成:①遍历处理每个元素,O(n) 级别的普通遍历;②每个桶内再次排序的时间复杂度总和。假设有 n 个待排序数字。分到 m 个桶中,如果分配均匀这样平均每个桶有 n/m 个元素。
对于第一个部分,最后排好序的取值遍历一趟的 O(n)。而第二部分分析:
如果桶内元素分配较为均匀假设每个桶内部使用的排序算法为快速排序,那么每个桶内的时间复杂度为(n/m) log(n/m)
。有 m 个桶,那么时间复杂度为m * (n/m)log(n/m)=n (log n-log m)
。所以最终桶排序的时间复杂度为: ·、O(n)+O(n*(log n- log m))
=O(n+n*(log n -log m))
。有时也会写成O(n+c),其中 c=n*(log n -log m)
。
极限情况 n=m 时,就能确保避免桶内排序,将数值放到桶中不需要再排序达到 O(n) 的排序效果,当然这种情况属于计数排序。
5️⃣桶排序适用情况
桶排序并不像常规排序那样没有限制,桶排序有相当的限制。因为桶的个数和大小都是人为设置的。而每个桶又要避免空桶的情况。所以在使用桶排序的时候既需要对待排序数列要求偏均匀,又要要求桶的设计兼顾效率和空间。
- 待排序序列不均匀的情况:
这样其实相当于只用了有效的很少个数桶,而再看桶排序的时间复杂度:O(n+n*(log n -log m))
。m 趋向 1,log m 趋向 0。整个复杂度变成O(n+nlogn)
。从级别来看就是O(nlogn)
,这种情况就跟没用桶一样,就是快排 (或其他排序) 的时间复杂度。
- 搞 100000 个桶的情况:
这才短短不到 100 个数,为了一一映射用 100000 个空间,浪费空间,而且遍历虽然 O(n) 也是 100000 次,这比 100 个的 O(nlogn) 大很多。
所以数要相对均匀分布,桶的个数也要合理设计。在设计桶排序时,需要知道输入数据的上界和下界,看看数据的分布情况,再考虑是否用桶排序,当然如果能用好桶排序,效率还是很高的!
6️⃣代码实现
序列:1 8 7 44 42 46 38 34 33 17 15 16 27 28 24。选用 5 个桶进行桶排序:
public class BucketSort {
public static void main(String[] args) {
int a[] = {1, 8, 7, 44, 42, 46, 38, 34, 33, 17, 15, 16, 27, 28, 24};
List[] buckets = new ArrayList[5];
//初始化
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<Integer>();
}
//将待排序序列放入对应桶中
for (int i = 0; i < a.length; i++) {
int index = a[i] / 10;//对应的桶号
buckets[index].add(a[i]);
}
//每个桶内进行排序(使用系统自带快排)
for (int i = 0; i < buckets.length; i++) {
buckets[i].sort(null);
//顺便打印输出
for (int j = 0; j < buckets[i].size(); j++) {
System.out.print(buckets[i].get(j) + " ");
}
}
}
}
桶排序最好情况下使用线性时间 O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n2)
十一、总结
1️⃣排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
2️⃣算法对比图:
3️⃣关于时间复杂度:
- 【平方阶 (O(n^2)) 排序】各类简单排序:直接插入、直接选择和冒泡排序。
- 【线性对数阶 (O(nlog2n)) 排序】快速排序、堆排序和归并排序。
- 【O(n1+§)) 排序】§ 是介于 0 和 1 之间的常数。 希尔排序。
- 【线性阶 (O(n)) 排序】基数排序,此外还有桶、箱排序。
4️⃣关于稳定性
- 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
- 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
5️⃣名词解释:
- n:数据规模
- k:“桶” 的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存