常见的几种排序算法
排序算法按一次能够处理的数据量的分为:外排序和内排序。
外排序:指能够处理大量数据的排序算法,通常来说就是不能够一次装入内存,只能放在读写较慢的外存储器上(通常是硬盘上)。
内排序:内排序是指待排序完全放入内存中进行排序的过程,适合不太大的元素序列。
常见的几种排序算法:
- 交换排序
- 冒泡排序
- 快速排序
- 选择排序
- 简单选择排序
- 堆排序
- 插入排序
- 直接插入排序
- 希尔排序
- 归并排序
- 基数排序
各大排序算法的稳定性:
(一)冒泡排序
冒泡排序(英语:Bubble Sort)又称为泡式排序,是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
理解:相邻的比较,递增就不动,递减就交换,一轮过后,该序列中的最大值就已经在最右端。再次重复操作,每经过一轮都能选出待排序序列的最大值。
上码:
/** * 冒泡排序 * 时间复杂度O(n²) 空间复杂度O(1) */ public static void BubbleSort(int[] a) { int temp = 0; for (int i = 0; i < a.length; i++) { int flag = 0; for (int j = 0; j < a.length - 1; j++) { if (a[j + 1] < a[j]) { temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; flag = 1; } } //当序列已经有序,就可以直接结束战斗 if (flag == 0){ break; } } }
改进:鸡尾酒排序
是冒泡排序的一种变形。他可以得到比冒泡排序稍微好一点的性能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目,而该算法是从低到高然后从高到低。
在随机数序列的状态下,鸡尾酒排序与冒泡排序的效率与其他众多排序算法相比均比较低。
(二)简单选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
思路描述:通过相邻的比较和交换,每次找个最小(大)值。
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的首位(末尾)。
- 以此类推,直到所有元素均排序完毕。
上码:
/** * 选择排序 * 时间复杂度O(n²) 空间复杂度O(1) */ void SelectSort(int arr[]) { int temp = 0; for (int i = 0; i < arr.length; i++) { int index = i; for (int j = i + 1; j <arr.length; j++) { if (arr[j] < arr[index]) { index = j; } } if (index == i) continue; else { temp = arr[index]; arr[index] = arr[i]; arr[i] = temp; } } }
(三)直接插入排序
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,可以理解为玩扑克牌时的理牌;在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法描述:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
上码:
/** * 直接插入排序 * 时间复杂度O(n²) 空间复杂度O(1) */ public static void straightInsertion(int[] arr) { int current;//要插入的数 for (int i = 1; i < arr.length; i++) { //从1开始 第一次一个数不需要排序 current = arr[i]; int j = i - 1;//已排好序的序列元素个数 while (j >= 0 && arr[j] > current) {//从后往前循环,将大于当前插入数的向后移动 arr[j + 1] = arr[j];//元素向后移动 j--; } arr[j + 1] = current;//找到位置,插入当前元素 } }
(四)希尔排序
希尔排序(Shellsort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于(直接)插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
算法描述:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行1次直接插入排序。
举个栗子,有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
然后我们对每列进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
排序之后变为:
10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94
最后以1步长进行排序(此时就是简单的插入排序了)。
从这个栗子可以看出 ,步长的选择是希尔排序的重要部分,再回看希尔排序也叫“递减增量排序”这里的增量 也就是我们所说的步长了,相比与冒泡和直接插入希尔排序能够让元素移动的步伐迈的更大些。选择一个初始的步长,再逐轮递减步长,当步长为1时 ,必然就是简单的插入排序了。
上码:
public static void shellSort(int[] arr) { int length = arr.length; int temp; for (int step = length / 2; step >= 1; step /= 2) { for (int i = step; i < length; i++) { temp = arr[i]; int j = i - step; while (j >= 0 && arr[j] > temp) { arr[j + step] = arr[j]; j -= step; } arr[j + step] = temp; } } }
(五)归并排序
归并排序(MergeSort),是创建在归并操作上的一种有效的排序算法。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
采用分治法:
- 分割:递归地把当前序列平均分割成两半。
- 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。
算法描述:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别再采用归并排序,继续分割下去,直至可以直接计算结果;
- 将两个排序好的子序列合并成一个排序序列后,继续合并其他子序列。直至剩下最终的一个排序序列。
归并操作:
- 递归法:
static void merge_sort_recursive(int[] arr, int[] result, int start, int end) { if (start >= end) return; int len = end - start, mid = (len >> 1) + start; int start1 = start, end1 = mid; int start2 = mid + 1, end2 = end; merge_sort_recursive(arr, result, start1, end1); merge_sort_recursive(arr, result, start2, end2); int k = start; while (start1 <= end1 && start2 <= end2) result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++]; while (start1 <= end1) result[k++] = arr[start1++]; while (start2 <= end2) result[k++] = arr[start2++]; for (k = start; k <= end; k++) arr[k] = result[k]; } public static void merge_sort(int[] arr) { int len = arr.length; int[] result = new int[len]; merge_sort_recursive(arr, result, 0, len - 1); }
- 迭代法:
public static void merge_sort(int[] arr) { int[] orderedArr = new int[arr.length]; for (int i = 2; i < arr.length * 2; i *= 2) { for (int j = 0; j < (arr.length + i - 1) / i; j++) { int left = i * j; int mid = left + i / 2 >= arr.length ? (arr.length - 1) : (left + i / 2); int right = i * (j + 1) - 1 >= arr.length ? (arr.length - 1) : (i * (j + 1) - 1); int start = left, l = left, m = mid; while (l < mid && m <= right) { if (arr[l] < arr[m]) { orderedArr[start++] = arr[l++]; } else { orderedArr[start++] = arr[m++]; } } while (l < mid) orderedArr[start++] = arr[l++]; while (m <= right) orderedArr[start++] = arr[m++]; System.arraycopy(orderedArr, left, arr, left, right - left + 1); } } }
(六)快速排序
算法步骤为:
- 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
- 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。
上码:
/** * 快速排序(挖坑法递归) * @param arr 待排序数组 * @param low 左边界 * @param high 右边界 */ public static void sort(int arr[], int low, int high) { if (arr == null || arr.length <= 0) { return; } if (low >= high) { return; } int left = low; int right = high; int temp = arr[left]; while (left < right) { while (left < right && arr[right] >= temp) { right--; } arr[left] = arr[right]; while (left < right && arr[left] <= temp) { left ++; } arr[right] = arr[left]; } arr[left] = temp; System.out.println("Sorting: " + Arrays.toString(arr)); sort(arr, low, left-1); sort(arr, left + 1, high); }
(七)堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
分为:
1、大根堆(大顶堆):每个节点的值都不大于其父节点的值,从下往上,节点的值越来越大,根节点的值最大。(多用于升序)
2、小根堆(小顶堆):每个节点的值都不小于其父节点的值,从下往上,节点的值越来越小,根节点的值最小。(多用于降序)
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:
补充:
完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,父结点:i => 子结点:2*i-1
基本思想:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
码:
public class HeapSort { public static void main(String []args){ int []arr = {4,6,8,5,9}; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int []arr){ //1.构建大顶堆 for(int i=arr.length/2-1;i>=0;i--){ //从第一个非叶子结点从下至上,从右至左调整结构 adjustHeap(arr,i,arr.length); } //2.调整堆结构+交换堆顶元素与末尾元素 for(int j=arr.length-1;j>0;j--){ swap(arr,0,j);//将堆顶元素与末尾元素进行交换 adjustHeap(arr,0,j);//重新对堆进行调整 } } /** * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)*/ public static void adjustHeap(int []arr,int i,int length){ int temp = arr[i];//先取出当前元素i for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始 if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点 k++; } if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换) arr[i] = arr[k]; i = k; }else{ break; } } arr[i] = temp;//将temp值放到最终的位置 } /** * 交换元素*/ public static void swap(int []arr,int a ,int b){ int temp=arr[a]; arr[a] = arr[b]; arr[b] = temp; } }
(八)基数排序
基数排序(Radix sort),是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
基本思想:对个位数先排序,再对十位数排序,以此类推。如果数据不满足位数相同,要对不够位数的数字前面补0(或者做类似处理)。时间复杂度O(nk)其中n为数字个数,k为最多的数字位数。
解析一下:该排序算法会进行x轮,x 为序列中最高位的位数(例如序列中最大的值为3位数,那x=3),从个位数对应位置的值开始,每轮都会进行排序,分别放入不同的桶中。
流程演示:
参考了大佬们的:
https://www.cnblogs.com/onepixel/articles/7674659.html
https://blog.csdn.net/weixin_37933986/article/details/72027038
https://juejin.im/post/6844903687932887053