【Java】—— 常见的排序算法
一、排序的概念
排序的定义:将一组数据按照特定的顺序重新排列的过程。
排序的目的:是使数据更易于查找、比较和分析。
排序的适应范围:可以应用于各种数据类型。
排序的稳定性:指当出现相同元素时,排序后相同元素的先后顺序保持不变,则称这个排序为稳定排序
内部排序:指在内存中进行排序
外部排序:指借助硬盘完成排序
二、常见排序算法的实现
1、直接插入排序
核心思想:
将数组分为 已排序区间 和 未排序区间 , 初始情况下,已排序区间就是空,未排序区间就是整个数组,每次从 未排序区间 中选择一个元素,插入到 已排序区间 的合适位置上。
代码实现:
private static void insertSort(int[] arr){//bound 变量表示边界//[0,bound) 区间表示有序区间//[bound,arr.length) 区间是无序区间for(int bound = 1; bound < arr.length ; bound++ ){//循环内部进行一次的插入操作//value 就是无序区间的第一个元素int value = arr[bound];//用这个元素在有序区间从后向前比较int cur = bound - 1;for(;cur >= 0 ; cur--){//前一个的值大于bound那么if(arr[cur] > value){arr[cur + 1] = arr[cur];}else {break;}}//交换元素arr[cur+1] = value;}}
特性:元素集合越接近有序,直接插⼊排序算法的时间效率越⾼
时间复杂度:O(n^2)
空间复杂度:O(1);
稳定性:稳定
2、希尔排序
核心思想:
希尔排序是对插入排序的一种优化,引入了 gap(间隔) 的概念,将元素分成多个组,并分别进行插入排序。再不断缩小 gap 直到 gap 为 1 时,就是普通的插入排序。
代码实现:
private static void shellSort(int[] arr){//计算一个合适的gap值,再不断缩小int gap = arr.length / 2;while (gap >= 1){insertSortGap(arr,gap);gap /= 2;}}private static void insertSort(int[] arr){//bound 变量表示边界//[0,bound) 区间表示有序区间//[bound,arr.length) 区间是无序区间for(int bound = 1; bound < arr.length ; bound++ ){//循环内部进行一次的插入操作//value 就是无序区间的第一个元素int value = arr[bound];//用这个元素在有序区间从后向前比较int cur = bound - 1;for(;cur >= 0 ; cur--){//前一个的值大于bound那么if(arr[cur] > value){arr[cur + 1] = arr[cur];}else {break;}}//交换元素arr[cur+1] = value;}}
希尔排序的时间复杂度不好计算,因为 gap 的取值有很多
3、直接选择排序
核心思想:
将元素划分为 有序区间 和 无序区间,初始情况下 有序区间 为 空 ,每一次遍历都从无序区间里找到最小值,把这个最小值依次放到有序区间
代码实现:
private static void selectSort(int[] arr){for(int bound = 0;bound < arr.length - 1;bound++){for (int cur = bound + 1 ; cur < arr.length ; cur++){if(arr[cur] < arr[bound]){int temp = arr[cur];arr[cur] = arr[bound];arr[bound] = temp;}}}}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
4、堆排序
核心思想:
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一 种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
代码实现:
private static void selectSort(int[] arr){for(int bound = 0;bound < arr.length - 1;bound++){for (int cur = bound + 1 ; cur < arr.length ; cur++){if(arr[cur] < arr[bound]){int temp = arr[cur];arr[cur] = arr[bound];arr[bound] = temp;}}}}private static void headSort(int[] arr){//1.建堆createHeap(arr);int bound = arr.length - 1;for(int i = 0; i < arr.length ; i++){//交换堆顶和待排序区间的最后一个元素int temp = arr[0];arr[0] = arr[bound];arr[bound] = temp;//重新调整堆//通过 bound 表示堆的元素个数shiftDown(arr,bound,0);//最后一个元素归入到 已排序区间bound--;}}private static void createHeap(int[] arr){//从最后一个非叶子节点触发,从后往前遍历for(int i=(arr.length-1-1)/2 ; i >= 0 ; i--){shiftDown(arr, arr.length, i);}}private static void shiftDown(int[] arr,int length,int index){int parent = index;int child = 2 * parent + 1;while (child < length){//建立大堆,需要取左右子节点中较大值if(child + 1 < length && arr[child + 1] > arr[child]){child += 1;}//比较 child 和 parent 元素大小if(arr[child] > arr[parent]){int temp = arr[child];arr[child] = arr[parent];arr[parent] = temp;}else{break;}parent = child;child = 2 * parent + 1;}}
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
5、冒泡排序
核心思想:
通过不断比较相邻元素,一趟过后,最大(最小)的一个元素就会在数组中最末尾的位置,再进行 n - 1 次就可以达到有序
代码实现:
private static void bubbleSort(int[] arr){for(int bound = 0;bound < arr.length-1 ; bound++){for (int cur = arr.length - 1 ; cur > bound ; cur--){if(arr[cur-1] > arr[cur]){int temp = arr[cur - 1];arr[cur - 1] = arr[cur];arr[cur] = temp;}}}}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
6、快速排序
核心思想:
任取待排序区间中选出某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
代码实现:
//此处约定[left,right]闭区间为待处理区间//快速排序入口private static void quickSort(int[] arr){quickSort(arr,0, arr.length-1);}//快速排序辅助递归的方法private static void quickSort(int[] arr,int left,int right){if(left >= right){//待处理区间为空或者只有一个元素,无需排序return;}//针对区间进行整理,返回值 index 表示基准值所在的下标int index = partition(arr,left,right);//递归左半区间quickSort(arr,left,index-1);//递归右半区间quickSort(arr,index+1,right);}private static int partition(int[] arr,int left,int right){//设定最右侧元素为基准值int value = arr[right];//设定两个下标,分别从左往右和右往左int l = left;int r = right;while (l < r){while (l < r && arr[l] <= value){l++;}while (l < r && arr[r] >= value){r--;}swap(arr,l,r);}//循环完成交换重合位置和基准值swap(arr,l,right);return l;}private static void swap(int[] arr,int i,int j){int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}
快速排序在大多数情况下的表现都是非常优秀的,除了个别极端情况会让时间复杂度到O(N^2)
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
7、归并排序
核心思想:
通过递归将待排序的数组分成两个子数组,直到每个子数组只包含一个元素。此时,单个元素的数组自然是有序的。将两个有序的子数组合并成一个有序的数组。合并时,从两个子数组的起始位置开始比较,选择较小的元素放入结果数组中,直到其中一个子数组的所有元素都被合并。然后将另一个子数组的剩余元素直接放入结果数组中。
代码实现:
//归并排序入口方法private static void mergeSort(int[] arr){mergeSort(arr,0,arr.length-1);}private static void mergeSort(int[] arr,int left,int right){//如果子区间没有元素或只有一个元素就不再递归if(left >= right){return;}//把区间分为等长的两个区间,不强制完全相等int mid = (left + right) / 2;//递归左半区间mergeSort(arr,left,mid);//递归右边区间mergeSort(arr,mid+1,right);//递归走到这,说明已经划分为只剩一个元素的有序数组//将有序数组合并merge(arr,left,mid,right);}private static void merge(int[] arr,int left,int mid,int right){//创建临时数组保存合并的结果,临时数组的长度应该是 right - left + 1int[] result = new int[right - left + 1];//通过判断大小尾插到新数组中int resultSize = 0;//两个下标指向每个区间的开头int cur1 = left;int cur2 = mid + 1;while (cur1 <= mid && cur2 <= right){if(arr[cur1] <= arr[cur2]){result[resultSize++] = arr[cur1];cur1++;}else{result[resultSize++] = arr[cur2];cur2++;}}//处理前面奇数划分不均的情况while (cur1 <= mid){result[resultSize++] = arr[cur1++];}while (cur2 <= right){result[resultSize++] = arr[cur2++];}//把临时数组的结果写回到 arr 的 [left,right] 中for(int i=0;i<result.length;i++){arr[left + i] = result[i];}}
归并排序的思考更多的是解决在磁盘中的外排序问题
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定
三、小结
除了上文提到的这几种,还有许多其他的排序算法,例如 计数排序 ,基数排序 ,桶排序 等等,应该结合实际情况选择使用,学好排序算法的原理可以用这些思想去解决其他的问题。