在本文中会整理并用代码实现常见的7种排序算法。 😏
常见的7种排序算法可以分为四大类:插入、选择、交换、归并,在此之外还有三种排序算法也很经典:计数排序、桶排序、基数排序。😶🌫️
常用排序算法介绍
零、前期准备
将常用操作写成方法:
//打印数组
public static void printArray(int[] arr) {
for (int x : arr) {
System.out.print(x+" ");
}
System.out.println();
}
//交换数字
public static void arrSwap(int[] arr,int i,int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
排序数组为:
int[] array = {5,8,3,4,1,10,7,2,9,6};
默认排为升序,排好顺序的序列为:1~10
一、插入排序
1、直接插入排序
插入排序算法应该是最好理解的一种算法,就是每次从乱序的数据中选择一个,在有序的序列中找到合适的位置插入,依然保证序列有序。
插入排序代码:
//插入排序
//空间复杂度:O(1)
//时间复杂度:O(n^2)
public static void insertSort(int[] arr){
//选择数组中的每一个元素
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int end = i-1;
//循环比较插入元素,arr[end]<key跳出,将key的值插入
while(end >= 0 && key < arr[end]){
arr[end+1] = arr[end];
end--;
}
arr[end+1] = key;
}
}
2、希尔排序
希尔排序也叫作递减增量排序算法,是对直接插入排序的一个改进,加入了组的概念对数据进行分组。
基本思想: 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
希尔排序中gap的取值影响着时间复杂度,所以希尔排序的时间复杂度不好确定。也没有一个确定的,有效的gap取值方法。
希尔排序代码:
//希尔排序
//空间复杂度:O(1)
//时间复杂度:不确定与gap有关
public static void shellSort(int[] arr){
int gap = arr.length;
while(gap>1) {
//gap的取值凭经验取值,不同的数据合适的gap可能存在差异
gap = gap/3+1;
for (int i = gap; i < arr.length; i++) {
int key = arr[i];
int end = i - gap;
while (end >= 0 && arr[end] > key) {
arr[end + gap] = arr[end];
end-=gap;
}
arr[end + gap] = key;
}
}
}
二、选择排序
1、直接选择排序
这种排序算法是一种十分直观的排序算法,无论是什么数据都是O(N2)的时间复杂度。
用在小规模数据效果较好。🧐
直接选择排序的代码实现:
//选择排序
public static void selectSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
int min = i;
//找出最大值位置
for (int j = i; j < arr.length; j++) {
if (arr[j]<arr[min]){
min = j;
}
}
//与该区间的第一个元素交换
if(min != i)
arrSwap(arr, i,min);
}
}
2、堆排序
堆排序是一种利用堆的概念进行排序的算法,分为两种
- 大顶堆: 用于排序升序序列,双亲节点的值均大于或等于孩子节点的值。
- 小顶堆: 用于排序降序序列,双亲节点的值均小于或等于孩子节点的值。
注: 大堆或者小堆中,孩子节点的大小相互之间没有要求,只要满足大堆和小堆的概念就行。
该算法的的最开始先进行建堆
堆排序代码实现:
//堆排序的向下调整 建大堆
public static void shiftDown(int[] arr, int size, int parent){
//child标记parent节点的左孩子
int child = parent*2+1;
//parent有左孩子时循环成立
while(child<size) {
//右孩子存在的情况下,找出左右孩子中最大的孩子
if (child+1 < size && arr[child+1] > arr[child]){
child += 1;
}
// 检验parent是否满足堆的特性
if(arr[parent]<arr[child]) {
arrSwap(arr, parent, child);
parent = child;
child = parent * 2 + 1;
}else {
return;
}
}
}
//堆排序
public static void heapSort(int[] arr){
//1.建堆
//找倒数的第一个非叶子节点
int size = arr.length;
int lastleaf = ((size-2)>>>1);
for (int root = lastleaf; root >=0 ; root--) {
shiftDown(arr,size,root);
}
//2.利用堆删除的思想排序,每次把堆顶最大的元素放在最后
//然后再对除最后一个元素的堆,再建大堆,选出最大元素,再放在末尾。
int end = size - 1;
while(end > 0){
arrSwap(arr,0,end);
shiftDown(arr,end,0);
end--;
}
}
三、交换排序
1、冒泡排序
冒泡排序是一个大家都非常熟悉的排序算法,也很简单直观,就像冒泡泡一样,每次比较两个元素,然后挨个交换。例如要排升序序列,则每次比较选出大的元素,让其往序列末尾交换,降序反之。
在冒泡排序中可以加入一个flag,来判断序列是否有序,如果有序,则不需要继续比较了。要不然排序算法还会一轮一轮不断去比较,没有实质的意义。
//冒泡排序
public static void bubbleSort(int[] arr){
int size = arr.length;
for (int i = 0; i < size; i++) {
//加入一个flag判断序列是否已经有序
boolean flag = false;
for (int j = 1; j < size-i; j++) {
if(arr[j-1]>arr[j]) {
arrSwap(arr, j-1, j);
flag = true;
}
}
//序列有序则结束
if (!flag)
return;
}
}
2、快速排序
- 快速排序通常比其它算法都要快,因为它的内部循环可以在大部分架构上有效的实现出来。
- 快排使用了分治的思想,挑出一个基准值,如果是升序,则将比基准值小的放在左边,大的放在右边,然后在左边和右边的序列中继续选定基准值进行分割,最后直到序列有序。
- 最好的情况就是选定的基准值刚好是有序序列的中间值。
基准值的选定方法一般是序列的首元素或者尾元素。
下图为前后指针的方法实现的快排:
在这里先不管序列分割如何实现,以基准值进行分割的方法会在2.1、2.2、2.3中给出,此处给出快排的基础代码:
递归实现
//快速排序
//递归写法,排序arr数组中left位置到right位置中间的元素
public static void quickSort(int[] arr,int left,int right){
if (right - left <= 1)
return;
//取到基准值的位置
int div = split_Up(arr,left,right);
//递归左半部分
quickSort(arr,left,div);
//递归右半部分
quickSort(arr,div+1,right);
}
非递归实现
//快速排序非递归写法 使用栈存放元素
public static void quickSort(int[] arr){
Stack<Integer> s = new Stack<>();
s.push(arr.length);
s.push(0);
while(!s.empty()){
int left= s.pop();
int right = s.pop();
if (right==0)
return;
int div = split_Up(arr,left,right);
s.push(right);
s.push(div+1);
s.push(div);
s.push(left);
}
}
2.1 Hoare方法
设定一个begin和一个end,一前一后,向对方移动。
假设基准值为尾元素(6),begin先走(也可以end先走),begin遇到比6大的元素则停下来,end遇到比6小的元素则停下,然后交换,以此类推,最终begin==end
两个指针相遇,将基准值放到相遇的位置,这样以6为基准值的划分就完成了,左边的元素都小于6,右边的都大于6。然后在以6为分界的左右两个序列中再选出基准值分别执行上述步骤。
下图为选择一个基准值6的分割过程:
代码实现:
//Hoare版
public static int split_Up(int[] arr,int left,int right){
int key = arr[right-1];
int begin = left;
int end = right-1;
while(begin < end){
while(begin < end && arr[begin] <= key)
begin++;
while(begin < end && arr[end] >= key)
end--;
if(begin != end)
arrSwap(arr,begin,end);
}
if(begin != right-1)
arrSwap(arr, begin, right-1);
return begin;
}
2.2 挖坑法
挖坑法实现就是,先将基准值保存入临时变量中,这就形成了一个“坑位”。begin先走,begin找到比基准值大的元素则填到“坑”里,这样begin位置又产生了一个坑,end找到比基准值小的元素再填到坑里,这样end位置就又产生一个坑,以此类推,直到二者相遇,将key中的存的基准值填入相遇位置,则划分完成。然后在以6为分界的左右两个序列中再选出基准值分别执行上述步骤。
下图为选择一个基准值6的分割过程:
代码实现:
//挖坑法
public static int split_Up(int[] arr,int left,int right){
int key = arr[right-1];
int begin = left;
int end = right-1;
while(begin < end){
//找到左边比key大的元素,填右边的坑
while(begin < end && arr[begin] <= key){
begin++;
}
if (begin < end)
arr[end] = arr[begin];
//找到右边比key小的元素,填左边的坑
while(begin < end && arr[end] >= key){
end--;
}
if (begin < end)
arr[begin] = arr[end];
}
arr[begin] = key;
return begin;
}
2.3 前后指针法
前后指针法就是设定两个指针cur和prev,cur在prev之后,遇到比基准值小的停下,prev遇到比基准值大的停下,然后交换,直到cur超出序列长度,交换基准值和prev所在位置的元素。
快排的动图也为前后指针法,只不过基准值选择为首元素。
下图为选择一个基准值6的分割过程:
代码实现:
//前后指针法
public static int split_Up2(int[] arr,int left,int right){
int key = arr[right-1];
int cur = left;
int prev = cur-1;
//循环让cur从前往后找比key小的元素
while(cur<right){
if (arr[cur] < key && ++prev != cur){
arrSwap(arr, prev, cur);
}
++cur;
}
if (++prev != right-1)
arrSwap(arr, prev, right-1);
return prev;
}
3、 优化的快速排序
既然快排中基准值的选择很重要,而且需要尽可能的保证接近有序序列的中间。则就可以使用三数取中的方法进行优化,也可以在序列的较小区间中使用插入排序。
3.1 三数取中代码实现,以Hoare方法为例
// 三数取中法:
public static int getMiddle(int[] array, int left, int right){
// left
// mid: left + ((right-left)>>1)
// right-1
int mid = left + ((right-left)>>1);
if(array[left] < array[right-1]){
if(array[mid] < array[left]){
return left;
}else if(array[mid] > array[right-1]){
return right-1;
}else{
return mid;
}
}else{
if(array[mid] > array[left]){
return left;
}else if(array[mid] < array[right-1]){
return right-1;
}else{
return mid;
}
}
}
//Hoare版
public static int split_Up(int[] arr,int left,int right){
int mid = getMiddle(arr, left, right);
if(mid != right-1){
arrSwap(arr, mid, right-1);
}
int key = arr[right-1];
int begin = left;
int end = right-1;
while(begin < end){
while(begin < end && arr[begin] <= key)
begin++;
while(begin < end && arr[end] >= key)
end--;
arrSwap(arr,begin,end);
}
arrSwap(arr, begin, right-1);
return begin;
}
3.2 在较小区间使用插入排序
此处以递归为例,在区间中47个元素的情况下使用插入排序,超过47个元素则继续使用快排。
注: 47的选择是参考快排的源码,源码中使用的是47作为分界。
//对插入排序做些更改
public static void insertSortQuick(int[] array, int left, int right){
for(int i = left+1; i < right; ++i){
int key = array[i];
int end = i-1;
while(end >= 0 && key < array[end]){
array[end+1] = array[end];
end--;
}
array[end+1] = key;
}
}
public static void quickSort(int[] array, int left, int right){
if(right - left < 47) {
insertSortQuick(array, left, right);
}else{
// 假设升序
// 找一个基准值将[left, right)区间分割成两个部分
int div = partition(array, left, right);
// 左侧部分比基准值小
// [left, div)
quickSort(array, left, div);
// 右侧部分比基准值大
// [div+1, right)
quickSort(array, div+1, right);
}
}
四、归并排序
归并排序是建立在归并操作基础上的一种排序算法,和快排一样也是采用分治的方法。
归并有两种实现方法:
- 自上而下的递归。
- 自下而上的迭代。
1、递归写法
自上而下的递归实现
归并排序的实现中,我借助了一个辅助数组temp。
//归并后进行排序的算法
public static void merge(int[] arr,int left,int mid,int right,int[] temp){
int begin1 = left, end1 = mid;
int begin2 = mid, end2 = right;
int index = left;
while(begin1 < end1 && begin2 < end2){
if (arr[begin1] <= arr[begin2]){
temp[index++] = arr[begin1++];
}else{
temp[index++] = arr[begin2++];
}
}
while(begin1 < end1)
temp[index++] = arr[begin1++];
while(begin2 < end2)
temp[index++] = arr[begin2++];
}
public static void mergeSort(int[] arr, int left,int right,int[] temp){
if(right-left>1){
int mid = left + ((right-left)>>1);
mergeSort(arr, left, mid, temp);
mergeSort(arr, mid, right, temp);
merge(arr,left,mid,right,temp);
System.arraycopy(temp,left,arr,left,right-left);
}
}
2、非递归实现
自下而上的迭代实现
归并排序的实现中,我借助了一个辅助数组temp。
//归并后进行排序的算法
public static void merge(int[] arr,int left,int mid,int right,int[] temp){
int begin1 = left, end1 = mid;
int begin2 = mid, end2 = right;
int index = left;
while(begin1 < end1 && begin2 < end2){
if (arr[begin1] <= arr[begin2]){
temp[index++] = arr[begin1++];
}else{
temp[index++] = arr[begin2++];
}
}
while(begin1 < end1)
temp[index++] = arr[begin1++];
while(begin2 < end2)
temp[index++] = arr[begin2++];
}
public static void mergeSort(int[] arr){
int gap = 1;
int size = arr.length;
int[] temp = new int[arr.length];
while (gap < size){
for (int i = 0; i <= size; i+=2*gap) {
int left = i;
int mid = left+gap;
int right = mid+gap;
if (right>size)
right=size;
if (mid>size)
mid=size;
merge(arr,left,mid,right,temp);
}
System.arraycopy(temp,0,arr,0,arr.length);
gap<<=1;
}
}
五、拓展
1、计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
代码实现:
//计数排序
public static void countSort(int[] arr){
//找出序列范围
int max = arr[0];
int min = arr[0];
for (int i = 0; i < arr.length; i++) {
if (max < arr[i])
max = arr[i];
if (min > arr[i])
min = arr[i];
}
//统计排序
int[] count = new int[max - min + 1];
for (int i = 0; i < arr.length; i++) {
count[arr[i]-min]++;
}
//排好序的序列写入arr
int index = 0;
for (int i = 0; i < count.length; i++) {
while(count[i]>0){
arr[index++] = i + min;
count[i]--;
}
}
}
2、桶排序
3、基数排序
注: 对排序算法想要更加系统了解的可以看下:https://github.com/hustcc/JS-Sorting-Algorithm