排序算法
一、排序算法概述
1、排序的介绍
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程
2、排序的分类
1)存储介质
内部排序:数据量不大,数据在内存
外部排序:数据量大,数据在外存,分批调入内存
2)比较器个数
串行排序:单处理机
并行排序:多处理机
3)主要操作
比较排序:用比较的方法
基数排序:根据取值确定有序位置
4)辅助空间
原地排序:空间O(1),无辅助空间
非原地排序:空间不是O(1)
5)稳定性
稳定性只对结构类型数据有意义
稳定排序:相等元素排序后次序不变
非稳定排序:相等元素排序后次序发生了变化
6、自然性
自然排序:原来的数据有序,排序速度很快
非自然排序:原来的数据有序,但排序速度却变慢了
二、算法的复杂度
1、时间度量方法
- 事后统计法
- 事前估计法
2、计算时间复杂度
- 用常数1代替运行时间中的所有加法常数
- 修改后的运行次数两数中,只保留最高阶项
- 去除最高阶项的系数
3、常见的时间复杂度
- 常数阶 O(1)
- 对数阶 O(log2n)
- 线性阶 O(n)
- 线性对数阶 O(nlog2n)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k次方阶 O(n^k)
- 指数阶 O(2^n)
4、平均时间复杂度和最坏时间复杂度
平均时问复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。
平均时间复杂度和最坏时间复杂度是否一致,和算法有关
5、算法的空间复杂度
类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模口有关,它随着口的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法,基数排序就属于这种情况
在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis、memcache)和算法(基数排序)本质就是用空间换时间
三、排序算法
1、冒泡排序
1)介绍
冒泡排序(Bubble Sorting):通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
2)优化
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 冒泡排序
* @author 王铭颢
* @date 2022/7/2 10:25
*/
public class BubbleSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
for(int i = 0; i < num.length; i++){
boolean flag = true;
for (int j = 0; j < num.length - i - 1; j++) {
if(num[j] > num[j+1]){
// 交换
int temp = num[j];
num[j] = num[j+1];
num[j+1] = temp;
// 标记
flag = false;
}
}
if(flag){
break;
}
}
System.out.println(Arrays.toString(num));
}
}
2、选择排序
1)介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
2)思想
后面序列中找到最值与前面元素互换
- 选择排序一共有“数组大小 - 1”轮排序
- 每轮排序,又是一个循环,循环的规则
- 先假定当前这个数是最小数
- 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
- 当遍历到数组的最后时,就得到本轮最小数和下标
- 交换
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 选择排序
* @author 王铭颢
* @date 2022/7/2 10:24
*/
public class SelectSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
for (int i = 0; i < num.length; i++) {
// 找最小值
int min = i;
for (int j = i + 1; j < num.length; j++) {
min = num[j] > num[min] ? min :j;
}
// 交换
int temp = num[i];
num[i] = num[min];
num[min] = temp;
}
System.out.println(Arrays.toString(num));
}
}
3、插入排序
1)介绍
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
2)思想
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 插入排序
* @author 王铭颢
* @Date 2022/7/2 10:35
*/
public class InsertSort {
public static void main(String[] args) {
int[] num = new int[]{10,9,8,7,6,5,4,3,2,1};
System.out.println(Arrays.toString(num));
for (int i = 1; i < num.length; i++) {
// 取出无需表首元素
int temp = num[i];
// 把temp找位置插入
for (int j = i ; j >= 0; j--) {
// 找到大于前一个元素的位置插入
// 如果是最小值,j == 0,插入最前面
if(temp > num[j] || j == 0){
num[j] = temp;
break;
}
// 元素后移(对于插入元素已经存在temp中了)
num[j] = num[j - 1];
}
}
System.out.println(Arrays.toString(num));
}
}
4、希尔排序
1)介绍
希尔排序是希尔 (Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
2)基本思想
插入排序的问题:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 希尔排序
*
* @author 王铭颢
* @Date 2022/7/3 08:51
*/
public class ShellSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
shell_2(num);
System.out.println(Arrays.toString(num));
}
/**
* 希尔排序_交换法
*
* @param num 待排序数组
*/
public static void shell_1(int[] num) {
// 分组
// gap:每组的元素个数
for (int gap = num.length / 2; gap > 0; gap /= 2) {
// 从最后一组的第一个元素开始,依次与上一组的对应元素比较并交换
for (int i = num.length - gap - 1; i < num.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (num[j] > num[j + gap]) {
int temp = num[j];
num[j] = num[j + gap];
num[j + gap] = temp;
}
}
}
}
}
/**
* 希尔排序_移位法
*
* @param num 待排序数组
*/
public static void shell_2(int[] num) {
int count = 0;
// 分组
// gap:每组的元素个数
for (int gap = num.length / 2; gap > 0; gap /= 2) {
// 从第二组开始
for (int i = gap; i < num.length; i++) {
// 记录被操作的值
int temp = num[i];
// 移动前面元素
for (int j = i; j >= 0; j -= gap) {
// 找到前一个位置比自己小的,否则向后移动元素
if (j - gap < 0 || num[j - gap] < temp) {
num[j] = temp;
break;
} else {
num[j] = num[j - gap];
}
}
}
}
}
}
5、快速排序
1)介绍
快速排序(Quicksort)是对冒泡排序的一种改进算法
2)基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 快速排序
*
* @author 王铭颢
* @Date 2022/7/3 11:48
*/
public class QuickSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
quickSort(num, 0, num.length - 1);
System.out.println(Arrays.toString(num));
}
private static void quickSort(int[] num, int left, int right) {
if (left >= right) {
return;
}
int l = left;
int r = right;
int pivot = num[(l + r) / 2];
while (l < r) {
while (num[l] < pivot) {
l++;
}
while (num[r] > pivot) {
r--;
}
if (l > r) {
break;
}
int temp = num[l];
num[l] = num[r];
num[r] = temp;
l++;
r--;
}
quickSort(num, 0, r);
quickSort(num, l, right);
}
}
6、归并排序
1)介绍
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略
分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。
2)思想
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 归并排序
*
* @author 王铭颢
* @Date 2022/7/3 22:54
*/
public class MergeSort {
public static void main(String[] args) {
int[] num = new int[]{12, 32, 45, 64, 83, 64, 23, 65, 10};
System.out.println(Arrays.toString(num));
int[] temp = new int[num.length];
mergeSort(num, temp, 0, num.length - 1);
System.out.println(Arrays.toString(num));
}
public static void mergeSort(int[] num, int[] temp, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(num, temp, left, mid);
mergeSort(num, temp, mid + 1, right);
merge(num, temp, left, mid, right);
}
}
public static void merge(int[] num, int[] temp, int left, int middle, int right) {
int i = left;
int j = middle + 1;
int t = 0;
// 1.把左右有序放在temp
while (i <= middle && j <= right) {
if (num[i] <= num[j]) {
temp[t++] = num[i++];
} else {
temp[t++] = num[j++];
}
}
// 2.把剩余的一方放到temp
while (i <= middle) {
temp[t++] = num[i++];
}
while (j <= right) {
temp[t++] = num[j++];
}
// 3.把temp复制到num
t = 0;
for (int ii = left; ii <= right; ii++) {
num[ii] = temp[t++];
}
}
}
7、基数排序(桶排序)
1)介绍
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort / bin sort)。它是通过键值的各个位的值,将要排序的元素分配至某些 “桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序是 1887年赫尔曼 • 何乐礼发明的。将整数按位数切割成不同的数字,然后按每个位数分别比较
2)思想
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。
这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
3)注意事项
- 基数排序是对传统桶排序的扩展,速度很快.
- 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成 OutOfMemory Error。
- 基数排序时稳定的。
- 数组不能有负数
4)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 基数排序
*
* @author 王铭颢
* @Date 2022/7/3 22:22
*/
public class RadixSort {
public static void main(String[] args) {
int[] num = new int[]{12,32,45,64,83,64,23,65,10};
System.out.println(Arrays.toString(num));
radixSort(num);
System.out.println(Arrays.toString(num));
}
private static void radixSort(int[] num) {
// 对个位
ArrayQueue aq[] = new ArrayQueue[10]; // 对象数组
for (int i = 0; i < 10; i++) {
aq[i] = new ArrayQueue(num.length);
}
for (int i = 0; i < num.length; i++) {
aq[num[i] % 10].addQueue(num[i]);
}
int count = 0;
for (int i = 0; i < 10; i++) {
while(!aq[i].isEmpty()){
num[count++] = aq[i].getQueue();
}
}
// 对十位
for (int i = 0; i < num.length; i++) {
aq[num[i] / 10].addQueue(num[i]);
}
count = 0;
for (int i = 0; i < 10; i++) {
while(!aq[i].isEmpty()){
num[count++] = aq[i].getQueue();
}
}
}
}
/**
* 使用数组模拟队列----编写一个ArrayQueue类
*/
class ArrayQueue {
private int maxSize; // 数组最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 队列的数据
/**
* 创建队列的构造器
*/
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[arrMaxSize];
front = -1;// 指向队列头的前一个位置
rear = -1;// 指向队列尾
}
/**
* 判断队列是否满
*/
public boolean isFull() {
return rear == maxSize - 1;
}
/**
* 判断队列是否为空
*/
public boolean isEmpty() {
return rear == front;
}
/**
* 添加队列数据
*/
public void addQueue(int n) {
if (isFull()) {
System.out.println("队列满,添加失败!");
return;
}
arr[++rear] = n;
}
/**
* 出队列
*/
public int getQueue() {
if (isEmpty()) {
// 抛出异常 -- 不需要写return
throw new RuntimeException("队列空");
}
return arr[++front];
}
/**
* 遍历
*/
public void showQueue() {
if (isEmpty()) {
System.out.println("队列空!");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d] = %d\n", i, arr[i]);
}
}
/**
* 显示头数据(不取出)
*/
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空");
}
return arr[front + 1];
}
}
四、常用排序总结对比
排序方法 | 时间-平均 | 时间-最好 | 时间-最坏 | 空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | n^2 | n | n^2 | 1 | 稳定 |
选择排序 | n^2 | n^2 | n^2 | 1 | 不稳定 |
插入排序 | n^2 | n | n^2 | 1 | 稳定 |
希尔排序 | nlogn | nlog^2 2 | nlog^2 2 | 1 | 不稳定 |
归并排序 | nlogn | nlogn | nlogn | n | 稳定 |
快速排序 | nlogn | nlogn | n^2 | logn | 不稳定 |
堆排序 | nlogn | nlogn | nlogn | 1 | 不稳定 |
基数排序 | n * k | n * k | n * k | n + k | 稳定 |