常见排序算法总结 -- java实现
排序算法可以分为两大类:
- 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
- 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
一、插入排序
1.1 直接插入排序
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。
直接插入排序的算法思路:
- 设置监视哨temp,将待插入记录的值赋值给temp;
- 设置开始查找的位置j;
- 在数组arr中进行搜索,搜索中将第j个记录后移,直至temp≥arr[j]为止;
- 将temp插入arr[j+1]的位置上。
/**
* 直接插入排序
*/
public void insertSort(int[] arr) {
//外层循环确定待比较数值
//必须i=1,因为开始从第二个数与第一个数进行比较
for (int i = 1; i < arr.length; i++) {
//待比较数值
int temp = arr[i];
int j = i - 1;
//内层循环为待比较数值确定其最终位置
//待比较数值比前一位置小,应插往前插一位
for (; j >= 0 && arr[j] > temp; j--) {
//将大于temp的值整体后移一个单位
arr[j + 1] = arr[j];
}
//待比较数值比前一位置大,最终位置无误
arr[j + 1] = temp;
}
}
1.2 希尔排序
希尔排序(Shell's Sort)是插入排序的一种,又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序的算法思路:
- 把数组按下标的一定增量分组;
- 对每组使用直接插入排序算法排序;
- 随着增量逐渐减少,每组包含的值越来越多,当增量减至1时,整个文件被分成一组,算法便终止。
/**
* 希尔排序
*/
public void shellSort(int[] arr) {
int d = arr.length;
while (d >= 1) {
d = d / 2;
for (int x = 0; x < d; x++) {
//按下标的一定增量分组然后进行插入排序
for (int i = x + d; i < arr.length; i = i + d) {
int temp = arr[i];
int j;
for (j = i - d; j >= 0 && arr[j] > temp; j = j - d) {
//移动下标
arr[j + d] = arr[j];
}
arr[j + d] = temp;
}
}
}
}
二、交换排序
2.1 冒泡排序
在一组数据中,相邻元素依次比较大小,最大的放后面,最小的冒上来。
冒泡排序算法的算法思路:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
/**
* 冒泡排序
*/
public void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// temp 临时存储 arr[j] 的值
int temp = arr[j];
//交换 arr[j] 和 arr[j+1] 的值
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
2.2 快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
通过一次排序将数组分成两个子数组,其中一个数组的值都比另外一个数组的值小,然后再对这两子数组分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序的算法思路:(分治法)
- 先从数列中取出一个数作为中间值middle;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 对左右两个小数列重复第二步,直至各区间只有1个数。
/**
* 快速排序
*
* @param arr 待排序数组
*/
public void quickSort(int[] arr) {
//查看数组是否为空
if (arr.length > 0) {
sort(arr, 0, arr.length - 1);
}
}
/**
* @param arr 待排序数组
* @param low 开始位置
* @param high 结束位置
*/
private void sort(int[] arr, int low, int high) {
if (low < high) {
int mid = getMiddle(arr, low, high); //将numbers数组进行一分为二
sort(arr, low, mid - 1); //对低字段表进行递归排序
sort(arr, mid + 1, high); //对高字段表进行递归排序
}
}
/**
* 查找出中轴(默认是最低位low)的在arr数组排序后所在位置
*
* @param arr 待排序数组
* @param low 开始位置
* @param high 结束位置
* @return 中轴所在位置
*/
private int getMiddle(int[] arr, int low, int high) {
int temp = arr[low]; //数组的第一个作为中轴
while (low < high) {
while (low < high && arr[high] >= temp) {
high--;
}
arr[low] = arr[high];//比中轴小的记录移到低端
while (low < high && arr[low] < temp) {
low++;
}
arr[high] = arr[low]; //比中轴大的记录移到高端
}
arr[low] = temp; //中轴记录到尾
return low; // 返回中轴的位置
}
三、选择排序
3.1 简单选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
简单选择排序的算法思路:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
- 以此类推,直到所有元素均排序完毕。
/**
* 简单选择排序
*/
public void selectSort(int[] arr) {
int minIndex = 0;
int temp;
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[i];
// 交换当前循环起点值和最小值索引位置的值
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
3.2 堆排序
堆排序(英语:Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。
堆排序的算法思路:
- 最大堆调整(Max Heapify):将堆的末端子节点作调整,某个节点的值最多和其父节点的值一样大;
- 创建最大堆(Build Max Heap):将堆中的所有数据重新排序,堆中的最大元素存放在根节点中;
- 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。
/**
* 堆排序
*/
public void heapSort(int[] arr) {
buildMaxHeap(arr);
//进行n-1次循环,完成排序
for (int i = arr.length - 1; i > 0; i--) {
//最后一个元素和第一个元素进行交换
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
// 筛选 R[0] 结点,得到i-1个结点的堆 将arr中前i-1个记录重新调整为大顶堆
heapAdjust(arr, 0, i);
}
}
/**
* 构建大顶堆
* <p>
* 将数组中最大的值放在根节点
*/
private void buildMaxHeap(int[] arr) {
for (int i = arr.length / 2; i >= 0; i--) {
heapAdjust(arr, i, arr.length - 1);
}
}
/**
* 堆调整
* <p>
* 将数组中最大的值放在根节点
*
* @param arr 待排序数组
* @param parent 父节点索引
* @param length 数组长度
*/
private void heapAdjust(int[] arr, int parent, int length) {
int temp = arr[parent]; //temp保存当前父节点
int child = 2 * parent + 1; //获取左子节点
while (child < length) {
// 如果有右子结点,并且右子结点的值大于左子结点的值,则选取右子结点的值
if (child + 1 < length && arr[child] < arr[child + 1]) {
child++;
}
// 如果父结点的值已经大于子结点的值,则直接结束
if (temp >= arr[child]) {
break;
}
// 把子结点的值赋给父结点
arr[parent] = arr[child];
// 选取子结点的左子结点,继续向下筛选
parent = child;
child = 2 * child + 1;
}
arr[parent] = temp;
}
四、归并排序
4.1 二路归并排序
归并排序(mergeSort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为二路归并。例如:将2个有序数组合并。比较2个数组的第一个数,谁小就先取谁,取了后就在对应数组中删除这个数。然后再进行比较,如果有数组为空,那直接将另一个数组的数依次取出即可。
二路归并排序的算法思路:
- 将数组分成A,B 两个数组,如果这2个数组都是有序的,那么就可以很方便的将这2个数组进行排序。
- 让这2个数组有序,可以将A,B组各自再分成2个数组。依次类推,当分出来的数组只有1个数据时,可以认为数组已经达到了有序。
- 然后再合并相邻的2个数组。这样通过先递归的分解数组,再合并数组就完成了归并排序。
/**
* 二路归并排序
*/
public void mergeSort(int[] arr) {
int[] temp = new int[arr.length]; //临时数组
sort(arr, temp, 0, arr.length - 1);
}
/**
* @param arr 待排序数组
* @param left 开始位置
* @param right 结束位置
*/
private void sort(int[] arr, int[] temp, int left, int right) {
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
sort(arr, temp, left, mid);
sort(arr, temp, mid + 1, right);
merge(arr, temp, left, mid, right);
}
/**
* 将两个有序表归并成一个有序表
*
* @param arr 待排序数组
* @param temp 临时数组
* @param leftStart 左边开始下标
* @param leftEnd 左边结束下标(mid)
* @param rightEnd 右边结束下标
*/
private static void merge(int[] arr, int[] temp, int leftStart, int leftEnd, int rightEnd) {
int rightStart = leftEnd + 1;
int tempIndex = leftStart; // 从左边开始算
int len = rightEnd - leftStart + 1; // 元素个数
while (leftStart <= leftEnd && rightStart <= rightEnd) {
if (arr[leftStart] <= arr[rightStart]) {
temp[tempIndex++] = arr[leftStart++];
} else {
temp[tempIndex++] = arr[rightStart++];
}
}
// 左边如果有剩余 将左边剩余的归并
while (leftStart <= leftEnd) {
temp[tempIndex++] = arr[leftStart++];
}
// 右边如果有剩余 将右边剩余的归并
while (rightStart <= rightEnd) {
temp[tempIndex++] = arr[rightStart++];
}
// 从临时数组拷贝到原数组
for (int i = 0; i < len; i++) {
arr[rightEnd] = temp[rightEnd];
rightEnd--;
}
}
五、计数排序
计数排序(Counting sort)不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序的算法思路:
- 求出待排序数组的最大值 max 和最小值 min。
- 实例化辅助计数数组temp,temp数组中每个下标对应arr中的一个元素,temp用来记录每个元素出现的次数。
- 计算 arr 中每个元素在temp中的位置 position = arr[i] - min。
- 根据 temp 数组求得排序后的数组。
/**
* 计数排序
*/
public void countSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
//找出数组中的最大最小值
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
int[] temp = new int[max];
//找出每个数字出现的次数
for (int i = 0; i < arr.length; i++) {
//每个元素在temp中的位置 position = arr[i] - min
int position = arr[i] - min;
temp[position]++;
}
int index = 0;
for (int i = 0; i < temp.length; i++) {
//temp[i] 大于0 表示有重复元素
while (temp[i]-- > 0) {
arr[index++] = i + min;
}
}
}
六、桶排序
桶排序 (Bucket sort)的工作原理是将数组分到有限数量的桶里。每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到 O(n log n) 下限的影响,桶排序可用于最大最小值相差较大的数据情况。
桶排序的算法思路:
- 找出待排序数组中的最大值max和最小值min;
- 我们使用动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为 (max-min) / arr.length + 1;
- 遍历数组 arr,计算每个元素 arr[i] 放的桶;
- 每个桶各自排序;
- 遍历桶数组,把排序好的元素放进输出数组。
/**
* 桶排序
*
* @param arr 待排序数组
*/
public static void bucketSort(int[] arr) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
//桶数
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketArr.add(new ArrayList<>());
}
//将每个元素放入桶
for (int i = 0; i < arr.length; i++) {
int num = (arr[i] - min) / arr.length;
bucketArr.get(num).add(arr[i]);
}
//对每个桶进行排序
for (int i = 0; i < bucketNum; i++) {
Collections.sort(bucketArr.get(i));
}
int position = 0;
//合并桶
for (int i = 0; i < bucketNum; i++) {
for (int j = 0; j < bucketArr.get(i).size(); j++) {
arr[position++] = bucketArr.get(i).get(j);
}
}
}
七、基数排序
基数排序(radix sort)是桶排序的扩展,基本思想是将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
基数排序的算法思路:
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点)。
/**
* 基数排序
*
* @param arr 待排序数组
*/
public void radixSort(int[] arr) {
int max = getMax(arr); // 数组arr中的最大值
for (int exp = 1; max / exp > 0; exp *= 10) {
//从个位开始,对数组arr按"exp指数"进行排序
countSort(arr, exp);
//bucketSort(arr, exp);
}
}
/**
* 获取数组中最大值
*/
private int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
/**
* 对数组按照"某个位数"进行排序(计数排序)
* <p>
* 例如:
* 1、当exp=1 表示按照"个位"对数组进行排序
* 2、当exp=10 表示按照"十位"对数组进行排序
*
* @param arr 待排序数组
* @param exp 指数 对数组arr按照该指数进行排序
*/
private void countSort(int[] arr, int exp) {
int[] temp = new int[arr.length]; // 存储"被排序数据"的临时数组
int[] buckets = new int[10];
// 将数据出现的次数存储在buckets[]中
for (int i = 0; i < arr.length; i++) {
buckets[(arr[i] / exp) % 10]++;
}
// 计算数据在temp[]中的位置 0 1 2 2 3 --> 0 1 3 5 8
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
// 将数据存储到临时数组temp[]中
for (int i = arr.length - 1; i >= 0; i--) {
temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
buckets[(arr[i] / exp) % 10]--;
}
// 将排序好的数据赋值给arr[]
for (int i = 0; i < arr.length; i++) {
arr[i] = temp[i];
}
}
/**
* 桶排序
*/
private void bucketSort(int[] arr, int exp) {
int[][] buckets = new int[10][arr.length]; //这是二维数组组成的桶
int[] counter = new int[10]; //此数组用来记录0-9每个桶中的数字个数,计数器
for (int i = 0; i < arr.length; i++) {
int index = (arr[i] / exp) % 10; //得出相应位置(如个位、十位)上的数字
buckets[index][counter[index]] = arr[i]; //取出来放到桶里
counter[index]++; //相应的计数器加1
}
int position = 0;
//合并桶
for (int i = 0; i < 10; i++) {
for (int j = 0; j < counter[i]; j++) {
arr[position++] = buckets[i][j];
}
}
}
源码地址:https://github.com/zt19994/leetcode/tree/master/src/Learn/SortAlgorithm