Java实现八大排序算法
-
算法特点
-
性能对比
参考博客:八大排序算法–Java实现
插入排序 — insertionSort
- 基本思想:每步将一个待排序的元素,按其值的大小插入前面已经排序的数组中适当的位置上,直到全部插入完为止。
- 注意在每次比较后都要移动。
public static void insertionSort(int[] arr){
for(int i = 1 ; i < arr.length ; i++){
int item = arr[i]; //记录当前元素值
int j = i - 1; //与当前元素进行比较的元素位置
//从后往前遍历找出第一个小于等于item的元素,其余元素都需要向后移动一位
//(如果用大于等于则为降序)(这里的等于号保证了稳定性)
for(; j >= 0 ; j--){
if(item < arr[j]) arr[j+1] = arr[j]; //移动
else break; //找到
}
arr[j+1] = item; //item应在改元素的后面
}
}
冒泡排序 — bubbleSort
- 基本思想:持续比较相邻的元素。每一轮把最大的数放在最右。
- 如果一轮中没有发生交换则说明已经有序,可以提前退出。
public static void bubbleSort(int[] arr){
for(int i = arr.length - 1; i > 0; i--) { //arr[i]放这一轮中比较得到的最大的元素
boolean flag = true; //是否发生交换的标志,用来提前退出
for(int j = 0 ; j < i ; j++){
if(arr[j]>arr[j+1]) { //需要交换(如果填小于则降序排列)
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = false;
}
}
if(flag==true) break; //提前退出
}
}
选择排序 — selectSort
- 基本思想:每轮从待排序数组中选择一个最小的元素,放到待排数组的头部(与第一个元素交换)。
- 理解选择排序的不稳定性:举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
public static void selectSort(int[] arr){
for(int i = 0 ; i < arr.length - 1 ; i++){
// 分别记录最小元素值及其下标
int nowmin = arr[i];
int minindex = i;
for(int j = i + 1 ; j < arr.length ; j++) {
if(nowmin>arr[j]) { //更新最小元素值和下标
nowmin = arr[j];
minindex = j;
}
}
if(i!=minindex) { //需要把最小元素和头元素进行交换
arr[minindex] = arr[i];
arr[i] = nowmin;
}
}
}
希尔排序 — shellSort
- 基本思想:取一个小于n的整数d作为第一个增量,把所有元素进行分组。所有距离为d的倍数的元素放在同一组,在各组内进行插入排序;然后,将增量减半(d = d/2)重复上述的分组和排序,直至增量d=1,即所有元素放在同一组中进行直接插入排序。
- 本质上是多次插入排序,与插入排序的不同在于,插入排序在数组尾部出现很小的元素时需要把前面所有元素都移动一次,而希尔排序通过分组的形式让数组尾部的小元素也能很快的到达头部。具体可以参考为什么希尔排序完虐直接插入排序。
堆排序 — heapSort
- 基本思想:利用大根堆的顺序存储结构(即对于节点 i 来说,左子节点为 2i+1,右子节点为 2i+2),每次将大根堆的堆顶元素与数组尾部元素进行交换,然后再次调整大根堆,反复循环直到最后一个节点为止。
- 对于每个父节点,需要比较其左右子节点是否大于父节点,如果大则交换,并且再重复调整交换的子节点的子树。
- 复杂度分析:构建初始的大顶堆的过程时间复杂度为O(n),交换及重建大顶堆的过程中,需要交换n-1次,重建大顶堆的过程根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以它最好和最坏的情况时间复杂度都是O(nlogn),空间复杂度O(1)。
- 堆排可以用来解决#TopK#问题,时间复杂度为O(NlogK)。(对于输出TopK不需要排序的问题用快排更快)
//堆排序
public static void heapSort(int[] a) {
//构建初始大根堆
for(int i = a.length/2 - 1; i >= 0; i--){ //找到倒数第一个非叶子结点
maxHeapDown(a, i, a.length-1);
}
//把堆顶最大元素与末尾元素进行交换,重新调整大根堆
//以i = a.length-1为例,交换后确定了最大值的位置,并且1到end-1中的树都满足大根堆,只需要再调整0下标为大根堆即可
for(int i = a.length-1; i > 0; i--){
int temp = a[0];
a[0] = a[i];
a[i] = temp;
maxHeapDown(a,0,i-1);
}
}
public static void maxHeapDown(int[] a, int start, int end) { //把start到end调整为大根堆,前提是start+1到end已经为大根堆
// 先根据堆性质,找出它左右节点的索引
int left = 2 * start + 1;
int right = 2 * start + 2;
// 默认当前节点(父节点)是最大值。
int maxNumIndex = start;
if (left <= end && a[left] > a[maxNumIndex]) maxNumIndex = left; // 如果有左节点,并且左节点的值更大,更新最大值的索引
if (right < end && a[right] > a[maxNumIndex]) maxNumIndex = right; // 如果有右节点,并且右节点的值更大,更新最大值的索引
//如果最大值不是当前非叶子节点的值,那么就把当前节点和最大值的子节点值互换
if (maxNumIndex != start) {
//交换
int temp = a[start];
a[start] = a[maxNumIndex];
a[maxNumIndex] = temp;
// 因为互换之后,子节点的值变了,如果该子节点也有自己的子节点,仍需要再次调整。
maxHeapDown(a, maxNumIndex, end);
}
}
归并排序 — mergeSort
- 基本思想:先拆分后合并。首先将数组拆分成左右两个数组,将这两个数组分别排序后再合并成一个有序数组(递归思想)。
- 拆分到只有一个数时停止;合并时需要申请一个临时数组来存放排序结果。
- 归并排序可以用来求逆序对数剑指 Offer 51. 数组中的逆序对
//归并排序
public static void mergeSort(int[] a, int start, int end) {
if(start < end){
int mid = (start+end)/2;
//拆分
mergeSort(a,start,mid);
mergeSort(a,mid+1,end);
//合并
merge(a,start,mid,mid+1,end);
}
}
public static void merge(int[] a, int leftStart, int leftEnd, int rightStart, int rightEnd){
int[] temp = new int[a.length]; //存放合并后的数组
int tempPos = leftStart, leftPos, rightPos; //分别记录左数组右数组与临时数组的当前位置
for(leftPos = leftStart, rightPos = rightStart; leftPos<=leftEnd && rightPos<=rightEnd;){
temp[tempPos++] = a[leftPos]<a[rightPos]? a[leftPos++]:a[rightPos++];
}
while(leftPos<=leftEnd) temp[tempPos++] = a[leftPos++]; //加载剩余的数
while(rightPos<=leftEnd) temp[tempPos++] = a[rightPos++];
for(int i = leftStart; i <= rightEnd; i++) a[i] = temp[i]; //拷贝答案
}
快速排序 — quickSort
- 基本思想:每次找出关键元素在数组中的位置,把小于关键元素的数放在左边,大于的放在右边,再递归的排序左右两个数组。
- 关键元素的选取对应不同的最坏情况,比如选择第一个元素为关键元素会对基本有序的数组性能较差。
- 快排的不稳定:举一个例子5,5,6,1,7。第一轮1被放到最前面,6放在1的位置,第一个5的会被放在原先6的位置,在第二个5后面,所以是不稳定的。
- 可以利用快排的特性来解决一些#TopK#问题,即只需要取出数组中的前K名,并且这K名没有顺序要求。973. 最接近原点的 K 个点、347. 前 K 个高频元素解决这类问题的时间复杂度为O(N)。
//快速排序
public static void quickSort(int[] a, int start, int end){
//每次找出关键元素在数组中的位置
//把小于关键元素的数放在左边,大于的放在右边,再递归的排序左右两个数组
if(start < end) {
//返回关键元素的当前位置
int nowKeyIndex = partition(a, start, end);
quickSort(a,start,nowKeyIndex-1);
quickSort(a,nowKeyIndex+1,end);
}
}
public static int partition(int[] a, int start, int end){
//关键元素的选择会出现不同的最坏情况,这里选择的是第一个元素
int key = a[start]; //将关键元素的值保存在key中
int i = start, j = end;
while(i<j){
while(i<j && a[j] >= key) j--; //找到右边比关键元素小的数
a[i] = a[j]; //直接覆盖
while(i<j && a[i] <= key) i++; //找到左边比关键元素大的数
a[j] = a[i];
}
a[i] = key; //将关键元素赋值在对应位置上
return i; //返回关键元素所在位置
}
TopK代码,partition部分不需要改,只需要在递归快排的时候根据nowKeyIndex的结果排一边即可。
public void getTopKByQuickSort(int[] arr, int start, int end, int k){
if(start<end){
int nowKeyIndex = partition(arr,start,end);
//根据nowKeyIndex的位置排一边即可
if(nowKeyIndex>k) getTopKByQuickSort(arr, start, nowKeyIndex-1, k);
else if(nowKeyIndex<k) getTopKByQuickSort(arr, nowKeyIndex+1, end, k);
else return; //直接返回
}
}
public int partition(int[] arr, int start, int end){
int key = arr[start]; //记录哨兵
int i = start, j = end;
while(i<j){
while(i<j&&key<=arr[j]) j--;
arr[i] = arr[j];
while(i<j&&key>=arr[i]) i++;
arr[j] = arr[i];
}
arr[i] = key;
return i;
}
桶排序 — bucketSort
-
基本思想:将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
-
桶排序有许多版本,一般只考桶排序的思想。
-
桶排序不是一种基于比较的排序方法;它是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素。
-
桶的内部可以使用空间来直接排序,也可以使用上面提到的不同排序算法来排序,若使用不同的排序算法,其稳定性继承自内部排序。