1.介绍
通过算法将指定的一组数据按照顺序进行排列的过程
1.1.分类
-
内排序:将需要处理的所有数据都加载到内存进行排序
-
外排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
1.2.常见的排序算法
1.3.时间复杂度
-
时间频度:一个算法花费的时间与算法中语句执行的次数成正比例,算法中语句执行次数越多,花费的时间越多。一个算法中语句的执行次数称为时间频度。记为
T(n)
-
时间复杂度:算法中的基本操作语句的执行次数是问题规模n的某个函数,用
T(n)
表示,若有某个辅助函数f(n)
,使n趋近于无穷大时,T(n)/f(n)
的极限值为不等于零的常数,则称f(n)
为T(n)
的同数量级函数。记作T(n) = O(f(n))
,称O(f(n))
为算法的渐近时间复杂度,简称时间复杂度 -
计算时间复杂度的方法:
- 如果运行时间是常数量级,用常数1表示;
- 只保留时间函数中的最高阶项;
- 如果最高阶项存在,则省去最高阶项前面的系数。
-
常见的时间复杂度
常数阶 O(1)
< 对数阶 O(logn)
< 线性阶 O(n)
< 线性对数阶 O(nlogn)
< 平方阶 O(n^2)
< 立方阶 O(n^3)
< k次方阶 O(n^k)
< 指数阶 O(2^n)
1.4.空间复杂度
空间复杂度是运行完一个程序所需要的内存的大小。这里包括了存储算法本身所需要的空间,输入与输出数据所占空间,以及一些临时变量所占用的空间。一般而言,我们只比较额外空间,来比较算法的空间优越性。
2.冒泡排序(Bubble Sorting)
- 介绍
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若逆序则交换,使值较大的元素逐渐向后移动
- 以数组 [24,69,80,57,13]为例:
- 一共进行数组长度 - 1 轮排序,每轮排序依次确定一个位置的数,每轮排序的比较次数在减少;
- 如果在某轮排序中没有发生交换,说明数组已经是有序的,可以提前结束冒泡排序
- 代码实现:
public static void BubbleSorting(int[] arr){
int temp = 0;//临时变量
boolean flag = false;//标志变量,记录是否进行了交换
for (int i = 0; i < arr.length - 1; i++) {//外层循环
for (int j = 0; j < arr.length - 1 - i; j++) {//内层循环
if (arr[j] > arr[j + 1]){//如果两个数逆序则交换
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第" + (i + 1) + "趟排序后的数组:");
System.out.println(Arrays.toString(arr));
if (!flag){
return;//没有交换说明数组已经有序,直接返回
}else {
flag = false;//发生过交换,重置标志变量
}
}
}
- 冒泡排序是稳定的排序算法
- 冒泡排序的最好时间复杂度:
O(n)
最坏时间复杂度:O(n^2)
,平均时间复杂度:O(n^2)
- 空间复杂度:
O(1)
3.*快速排序(Quick Sorting)
- 介绍
快速排序是对冒泡排序的一种改进。基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据都要小,然后再按这个方法对两部分分别进行快速排序,整个排序过程可以递归进行,以此达到整个序列称为有序序列
- 快速排序示意图
-
代码实现:
- 上图基准值取的最后一个,所以先从前遍历,下面的代码取第一个数作为基准值,所以后面的变量先遍历
//快速排序
public static void quickSorting(int[] arr,int left,int right){
if(left >= right){
return;
}
int low = left;
int high = rzight;
int temp = 0;//基准值
temp = arr[low];//基准值取第一个
while(low < high){
//尾部先向前查找小于基准值的元素
while(low < high && arr[high] >= temp){
high --;
}
//找到了就赋给left
arr[low] = arr[high];
//前端向后查找大于基准值的元素
while (low < high && arr[low] <= temp){
low ++;
}
//找到后赋值给right
arr[high] = arr[low];
}
//退出循环时left = right,在基准值正确的位置
arr[low] = temp;
//对基准值左右的序列分别进行排序
quickSorting(arr,left,low - 1);
quickSorting(arr,low + 1,right);
//System.out.println(Arrays.toString(arr));
}
- 快速排序是不稳定的排序算法
- 快速排序的最好时间复杂度:
O(nlogn)
,最坏时间复杂度:O(n^2)
,平均时间复杂度:O(nlogn)
- 最好情况空间复杂度:
O(logn)
,最坏情况空间复杂度:O(n)
4.选择排序(Select Sorting)
- 介绍
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止,得到一个从小到大(从大到小)的有序序列
- 选择排序思路分析
-
一共进行数组长度 - 1轮排序,每轮排序都有如下循环:
- 假定当前数是最小数
- 当前数和后面每个数进行比较,存入更小的数
- 遍历完序列,得到本轮最小数的下标
- 代码实现:
//选择排序
public static void selectSorting(int[] arr){
int minIndex = 0;//存储最小数的下标
int temp = 0;//交换变量
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]){
minIndex = j;
}
}
temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
System.out.println("第" + (i + 1) + "趟排序后的数组:");
System.out.println(Arrays.toString(arr));
}
}
- 选择排序是不稳定的排序算法
- 选择排序的最好时间复杂度:
O(n^2)
最坏时间复杂度:O(n^2)
,平均时间复杂度:O(n^2)
- 空间复杂度:
O(1)
5.*堆排序(Heap Sorting)
-
堆排序是利用堆这种数据结构设计的一种算法,是一种选择排序
-
堆是具有以下性质的完全二叉树:
- 大顶堆:每个结点的值都大于等于其左右孩子结点的值(
arr[i] >= arr[2 * i + 1] && arr[i] >= arr[2 * i + 2]
) - 小顶堆:每个结点的值都小于等于其左右孩子结点的值(
arr[i] <= arr[2 * i + 1] && arr[i] <= arr[2 * i + 2]
) - 一般升序采用大顶堆,降序采用小顶堆
- 大顶堆:每个结点的值都大于等于其左右孩子结点的值(
-
基本思想:
- 首先将待排序的数组构造成一个大顶堆,此时,整个数组的最大值就是堆结构的顶端
- 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
- 将剩余的n-1个数再构造成大顶堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
-
堆排序分析图
以数组 {3,6,8,5,7} 为例
- 构造堆
插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大顶堆结构
插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构
插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构
插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构
- 固定最大值再构造堆
我们已经得到一个大顶堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大顶堆
此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较
下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大顶堆,后面就是重复固定最大值,然后构造大根堆
顶端数7与末尾数3进行交换,固定好7
剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组
- 代码实现:
//堆排序
public static void heapSort(int[] arr) {
//构造大顶堆
heapInsert(arr);
int size = arr.length;
while (size > 1) {
//固定最大值
swap(arr, 0, size - 1);
size--;
//构造大顶堆
heapify(arr, 0, size);
}
System.out.println(Arrays.toString(arr));
}
//堆排序——构造大顶堆(通过新插入的数上升)
public static void heapInsert(int[] arr) {
for (int i = 1; i < arr.length; i++) {
//当前插入的索引
int currentIndex = i;
//父结点索引
int fatherIndex = (currentIndex - 1) / 2;
//如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
//然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
while (arr[currentIndex] > arr[fatherIndex]) {
//交换当前结点与父结点的值
swap(arr, currentIndex, fatherIndex);
//将当前索引指向父索引
currentIndex = fatherIndex;
//重新计算当前索引的父索引
fatherIndex = (currentIndex - 1) / 2;
}
}
}
//堆排序——将剩余的数构造成大顶堆(通过顶端的数下降)
public static void heapify(int[] arr, int index, int size) {
int left = 2 * index + 1;
int right = 2 * index + 2;
while (left < size) {
int largestIndex;
//判断孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[left] < arr[right] && right < size) {
largestIndex = right;
} else {
largestIndex = left;
}
//比较父结点的值与孩子中较大的值,并确定最大值的索引
if (arr[index] > arr[largestIndex]) {
largestIndex = index;
}
//如果父结点索引是最大值的索引,那已经是大顶堆了,则退出循环
if (index == largestIndex) {
break;
}
//父结点不是最大值,与孩子中较大的值交换
swap(arr, largestIndex, index);
//将索引指向孩子中较大的值的索引
index = largestIndex;
//重新计算交换之后的孩子的索引
left = 2 * index + 1;
right = 2 * index + 2;
}
}
//堆排序——交换数组中两个元素的值
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 堆排序是不稳定的排序算法
- 堆排序的最好时间复杂度:
O(nlogn)
最坏时间复杂度:O(nlogn)
,平均时间复杂度:O(nlongn)
- 堆排序的空间复杂度:
O(1)
6.插入排序(Insertion Sorting)
- 介绍
将待排序的序列看成一个有序序列和一个无序序列,初始时有序序列只有一个元素,无序序列有 n - 1 个元素,每次从无序序列中取出第一个元素,把它依次与有序表中的每一个元素比较,将它插入到适当的位置,使之成为新的有序表,直到无序表中没有元素。
- 插入排序分析图
- 进行序列长度 - 1 次排序
- 每次排序都为无序表中的第一个元素找到合适的位置
- 代码实现:
//插入排序
public static void insertionSorting(int[] arr) {
int insertValue = 0;//要排序的元素
int insertIndex = 0;//插入的位置
for (int i = 1; i < arr.length; i++) {
insertValue = arr[i];//无序表的第一个元素
insertIndex = i - 1;//从有序表的最后一个元素开始比较
//比插入元素大的值向后移
while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;//索引向前遍历
}
//退出循环说明找到插入的位置
arr[insertIndex + 1] = insertValue;
System.out.println("第" + i + "轮排序后:");
System.out.println(Arrays.toString(arr));
}
}
- 插入排序是稳定的排序算法
- 插入排序的最好时间复杂度:
O(n)
最坏时间复杂度:O(n^2)
,平均时间复杂度:O(n^2)
- 空间复杂度:
O(1)
7.*希尔排序(Shell Sorting)
- 介绍
希尔排序是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
- 基本思想:把序列按下标的一定增量分组,对每组使用简单插入排序;每次排序后折半缩小分组的组数再排序,随着分组的减少每组包含的关键词增多,当分组减至1时,整个序列成为一组,再进行最后一次简单插入排序,算法终止。
- 希尔排序分析图
- 代码实现
//希尔排序
public static void shellSorting(int[] arr){
int gap = arr.length / 2;//增量值
while (gap >= 1){//循环至序列分为一组,即增量为1
//从 i = gap 位置开始遍历每组数据
for (int i = gap; i < arr.length; i++) {
int index = i;//插入位置
int temp = arr[i];//保存要排序的数
//本组前一位数大于temp就向后移
while (index - gap >= 0 && arr[index - gap] > temp){
arr[index] = arr[index - gap];
index -= gap;
}
//退出循环说明找到了插入位置为index
arr[index] = temp;
}
//缩小增量
gap /= 2;
}
System.out.println(Arrays.toString(arr));
}
- 希尔排序是不稳定的排序算法
- 希尔排序的最好时间复杂度:
O(nlogn)
,最坏时间复杂度:O(n^2logn)
,平均时间复杂度:O(nlogn)
- 希尔排序空间复杂度:
O(1)
8.*归并排序(Merge Sorting)
- 介绍
归并排序是利用归并的思想实现的排序方法,采用的经典的分治策略。
-
归并排序分析图
- 分治过程分析:
在分的阶段可以理解为递归拆分子序列的过程
在治的阶段将两个已经有序的子序列合并成一个有序序列,上图是最后一次合并的步骤
- 代码实现
//归并排序
//归并——分离算法
public static void mergeSorting(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;//求中间索引
//递归分离左边数组
mergeSorting(arr, left, mid, temp);
//递归分离右边数组
mergeSorting(arr, mid + 1, right, temp);
//合并
merge(arr, left, mid, right, temp);
}
}
//归并——合并算法
/**
* @param arr:需排序的数组
* @param left:数组第一个索引
* @param mid:数组中间索引
* @param right:数组最后一个元素的索引
* @param temp:辅助数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;//左边数组初始索引
int j = mid + 1;//右边数组初始索引
int t = left;//temp数组的初始索引
//左右数组比较元素把较小值存入temp,一边数组检测完为止
while (i <= mid && j <= right) {
temp[t++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
//退出循环说明有一边数组遍历完毕,另一边直接添加到temp
//左边数组还有剩余元素
while (i <= mid) temp[t++] = arr[i++];
//右边数组还有剩余元素
while (j <= right) temp[t++] = arr[j++];
//将temp数组复制到arr
for (int k = left; k <= right; k++) {
arr[k] = temp[k];
}
System.out.println(Arrays.toString(arr));
}
- 归并排序是稳定的排序算法
- 归并排序的最好时间复杂度:
O(nlogn)
最坏时间复杂度:O(nlogn)
,平均时间复杂度:O(nlogn)
- 空间复杂度:
O(n)
9.基数排序(Radix Sorting)
- 介绍
基数排序是桶排序(Bucket Sorting)的拓展,它是通过键值的各个位的值,将要排序的元素分配到某些“桶”中,达到排序的目的
- 桶排序的基本思想:创建若干数量的数组作为桶(一般为初始数组的长度),各个桶用来存放不同范围区间的元素,各个桶之间自排序,最后合并为一个有序的数组
- 基数排序基本思想:将所有待排序数统一为同样的数位长度,数位较短的前面补零,然后依次从最低位进行排序。从最低位一直排序到最高位以后,数列就变成了一个有序序列
- 基数排序分析图
以数组 {53,3,542,748,14,214}为例:
- 排序的轮次取决于数列中最大数的数位
maxLength
,例子中是百位,所以进行了3轮排序 - 需要1个有10个一维数组
bucket[][]
的二维数组存储各个位数对应的值,一维数组的长度为arr.length
- 需要1个数组
counts
记录每个位数对应的一维数组的元素个数(这个数组每轮要清空,用来覆盖上一轮的排序) - 遍历
arr
数组,将本次遍历的位数放入对应的桶中,遍历结束后按顺序从桶中取出元素放入arr
- 遍历全部有数据的桶(
counts
大于零说明本轮该桶中有数据),依次取出存放进arr
- 遍历
maxLength
次,arr
数组变为有序
- 代码实现:
//基数排序
public static void radixSorting(int arr[]) {
//找到元素的最大位数
int maxLength = 0;//最大位数
int max = 0;//最大的数
for (int i = 0; i < arr.length; i++) {//遍历数组找到最大的数
max = arr[i] > max ? arr[i] : max;
}
maxLength = (max + "").length();//求出最大数的位数
int[][] bucket = new int[10][arr.length];//作为桶的数组
int[] counts = new int[10];//记录每个桶内元素个数的数组
//整个循环进行maxLength次
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {//每轮循环取高一位,n的步长为10
//第 i 次循环,遍历数组arr
for (int j = 0; j < arr.length; j++) {
//求该轮的位数值
int Byte = arr[j] / n % 10;
//按位数值把该元素放入桶中
bucket[Byte][counts[Byte]++] = arr[j];
}
//一轮循环结束,按顺序从桶中取出元素放入arr
int index = 0;//辅助索引循环插入arr
for (int j = 0; j < bucket.length; j++) {
if (counts[j] > 0) {//如果该桶中有数据则进行遍历取出
for (int k = 0; k < counts[j]; k++) {
arr[index++] = bucket[j][k];
}
}
//将每个counts[j]重置
counts[j] = 0;
}
System.out.println("第" + (i + 1) + "轮:" + Arrays.toString(arr));
}
}
- 基数排序是稳定的排序算法
- 基数排序的最好时间复杂度:
O(n * k)
最坏时间复杂度:O(n * k)
,平均时间复杂度:O(n * k)
(k 为桶的个数) - 空间复杂度:
O(n + k)
10.总结与对比
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn) | O(nlogn) | O(n^2logn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(n) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | 稳定 |