本文章对常见的几种排序方法进行了归纳和整理
排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
一.插入排序
基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列,(比如一张一张对扑克牌进行排序)
1.直接插入排序
解释:该算法的基本思想是将待排序的元素插入到已经有序的序列中,初始时已排序序列只有一个元素,然后依次将未排序的元素插入到已排序序列的合适位置
代码如下:
class InsertSort{
public static void insertSort(int[] arr){
int n=arr.length;
//这里i是指每次你要插入的元素的位置,i从第二个开始
for (int i=1;i<n;i++){
//这里把要插入的元素的位置i的数字记录下来
int tmp=arr[i];
int j;
//j最开始是要插入的元素的位置.如果后面比tmp大,就把后面的值赋给j的位置,
//接着让j--,接着继续和后面比,直到j<1或已不需要调换
for(j=i;j>=1&&arr[j-1]>tmp;j--){
arr[j]=arr[j-1];
}
//这个时候把最开始的tmp赋给当前的arr[j](完成插入)
arr[j]=tmp;
}
}
}
(详细解释在注释中)
总的来说,就是每一轮把比要插入的元素大的元素向前挪,最后找到它应该在的的位置,然后把它插入
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
2.希尔排序( 缩小增量排序 )
定义:先选定一个增量整数gap,把待排序文件中所有记录分成多个组,即把所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后将gap减少重复上述分组和排序的工作。当gap=1时,当成直接插入排序,排序完成.(直接插入排序的优化版)
代码如下:
class ShellSort{
public static void shellSort(int[] arr){
int n=arr.length;
//确定增量gap,并在循环中使gap不断减小,直到gap==1
for(int gap=n/2;gap>0;gap/=2){
//这里i是指每次你要插入的元素的位置
//注意数组下标从0开始,所以当gap=2时,循环是从第三个开始的
for(int i=gap;i<n;i++){
//这里把要插入的元素的位置i的数字记录下来
int tmp=arr[i];
int j;
//j最开始是要插入的元素的位置,如果距离它gap位置的arr[j-gap]大于tmp,则直接把
//arr[j-gap]提到arr[j]的位置来,所以前面先用tmp把arr[j]的数字记录下来
//调整后j=j-gap,这时再看arr[j-gap]是否大于tmp,直到j<gap或不需要调整了
for(j=i;j>=gap&&arr[j-gap]>tmp;j-=gap){
arr[j]=arr[j-gap];
}
//这个时候把最开始的tmp赋给当前的arr[j](插入)
arr[j]=tmp;
}
//直到最后gap=1,当成直接插入排序,调整完毕
}
}
}
可以当成进行了几次直接插入排序
(可能有点难理解,如果有问题可以在评论区询问,我会快速回复的)
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
稳定性:不稳定
二.选择排序
基本思想:每一次从待排序的数据元素中选出最小或最大的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
1.直接选择排序
定义:在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素 ,若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换, 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
直接上代码
class Select Sort{
public static void selectSort(int[] arr){
//循环length-1次,每一次确定一个最小的数用minIndex记录下来
for(int i=0;i< arr.length-1;i++){
int minIndex=i;//前面数已经排好序了,所以从还没有排的第一个i开始
//从剩下的数找出最小的那一个
for(int j=i+1;j< arr.length;j++){
if(arr[j]<arr[minIndex]){
minIndex=j;
}
}
//如果i和minIndex不相等就把它们交换
if(i!=minIndex){
swap(arr,i,minIndex);
}
}
}
//交换函数
public static void swap(int[] arr,int index1,int index2){
int tmp=arr[index1];
arr[index1]=arr[index2];
arr[index2]=tmp;
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
2.堆排序
定义:堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆
(如果对堆这种数据结构不熟悉,可以参考另一篇博客:堆与优先级队列(Java)-CSDN博客)
(注意:参考博客建的是小堆,而我们这里升序排列,所以代码建的是大堆)
class HeapSort{
public static void heapSort(int[] arr){
//n代表的是数组的长度
int n=arr.length;
//建堆
for(int i=(n-1)/2;i>0;i--){
shiftDown(arr,n,i);
}
//i代表是还没有排序的数的个数
//每次将没排序的数中的最大值与最后一个数交换,同时使数组的长度减一,让第一个数重新进入堆
//其实相当于堆的删除,每次把最大的数找出来放在最后,如此反复就形成了升序
for(int i=n-1;i>0;i--){
int tmp=arr[0];
arr[0]=arr[i];
arr[i]=tmp;
shiftDown(arr,i,0);
}
}
public static void shiftDown(int[] arr,int n,int parent){
//看看有没有左孩子,没有就直接返回了
if(parent*2+1>n){
return;
}
//child为我们要处理的孩子(最开始是左孩子)
int child=parent*2+1;
while (child<n){
//看看有没有右孩子,有且右孩子大于左孩子的话,令child为右孩子
if(child+1<n&&arr[child+1]>arr[child]){
child++;
}
//如果parent>child,已经满足大堆的性质了,直接break,否则交换
if(arr[parent]>arr[child]){
break;
}else {
int tmp=arr[child];
arr[child]=arr[parent];
arr[parent]=tmp;
//parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent=child;
child=parent*2+1;
}
//由此进入下次循环,调整下个节点,直到满足特性
}
}
}
堆排序的特性总结
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
三.交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1.冒泡排序
class BubbleSort {
public static void bubbleSort(int[] array) {
int n = array.length;
boolean swapped;
//确定来n-1次排序,每次把一个较大的数放在后面
for (int i = 0; i < n - 1; i++) {
swapped = false;
//注意j<n-i-1,因为已经有i个数排好序了
for (int j = 0; j < n - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
swapped = true;
}
}
//已经有序,直接break
if (!swapped) {
break;
}
}
}
}
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
2.快速排序
定义:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序有很多的实现方式,但是它的主要框架如下
void QuickSort(int[] array, int left, int right) {
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
实现方式一:前后指针法
class QuickSort{
public static void quickSort(int[] arr,int left,int right){
if(left<right){
return;
}
//通过partition方法得到数组的中间值的下标,它的左边都比它小,右边都比它都大
int partitionIndex=partition2(arr,left,right);
//然后,我们继续调整中间值的左边部分
quickSort(arr,left,partitionIndex);
//调整中间值的右边部分
quickSort(arr,partitionIndex+1,right);
//最终,当递归调用结束时,我们的数组将按升序排序。
}
public static int partition(int[] arr,int left,int right){
//首先,我们选择left作为枢轴(中间值)
int pivot=arr[left];
//我们维护两个指针i和j,一个指向数组的起始位置(left),
//一个指向数组的结束位置(right)。然后,我们进入一个循环,直到指针相遇。
int i=left;
int j=right;
while (true){
//首先,我们从右边开始移动指针(j--),直到找到一个小于等于枢轴元素的元素
while (i<j&&arr[j]>pivot){
j--;
}
while (i<j&&arr[i]<pivot){
i++;
}
//如果指针i大于等于指针j,表示两个指针已经相遇,我们可以返回j作为分区索引。
//即调整完了,左边的都比arr[j]小,右边都比arr[j]大
if(j<=i){
return j;
}else {
//如果指针i小于指针j,表示我们找到了一对不正确的元素
//需要将它们交换位置,以确保所有小于枢轴元素的元素都位于枢轴的左边,
//所有大于枢轴元素的元素都位于枢轴的右边。
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
//在下个循环中继续调整,直到return j
}
}
}
}
实现方式二:Hoare法(展示实现partition方法的代码)
private static int partition2(int[] arr, int left, int right) {
//首先,我们选择left作为枢轴(中间值)
int pivot = arr[left];
int i = left;
int j = right;
while (i<j) {
//注意,这里判断条件包含了等于pivot,说明最左边的arr[left]并不会参与移动,
//为后面arr[left]与arr[j]交换做准备
while (i < j && arr[j] >= pivot) {
j--;
}
while (i < j && arr[i] <= pivot) {
i++;
}
if(i!=j){
swap(arr,i,j);
}
}
//此时arr[i]左边都比arr[left]小,右边都比arr[left]大,且arr[i]小于arr[left](一定是j--到i)
//把arr[left]放到正确的位置上
swap(arr,i,left);
return i;
}
public static void swap(int[] arr,int index1,int index2){
int tmp=arr[index1];
arr[index1]=arr[index2];
arr[index2]=tmp;
}
(hoare法的特点:每一次arr[left]并不参与移动,直到arr[left]的右边调整完了,再把arr[left]换过去)
实现方式三:挖坑法
private static int partition3(int[] array, int left, int right) {
int i = left;
int j = right;
int pivot = array[left];
while (i < j) {
while (i < j && array[j] >= pivot) {
j--;
}
//找到比中间值小的数,直接赋值到左边
array[i] = array[j];
while (i < j && array[i] <= pivot) {
i++;
}
//找到比中间值大的数,赋值到右边
array[j] = array[i];
}
//把中间值调换过去
array[i] = pivot;
return i;
}
(挖坑法的特点:本质与hoare法是一样的,只不过因为它利用了pivot存放了中间值,所以可以直接赋值而不用额外创建tmp,使代码更简洁了)
实现方式四:非递归快速排序
void quickSortNonR(int[] arr, int left, int right) {
//建立一个堆来存放每次要排序的左边和右边
Stack<Integer> st = new Stack<>();
st.push(left);
st.push(right);
while (!st.empty()) {
//得到左边和右边
right = st.pop();
left = st.pop();
if(right - left <= 1)
continue;
int div = PartSort1(arr, left, right);
// 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right),把它们存进栈
st.push(div+1);
st.push(right);
st.push(left);
st.push(div);
}
}
特点:使用了栈这种数据结构把每次排序的左边和右边存放起来,避免了递归的使用
快速排序总结
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
四.归并排序
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使 子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
1.归并排序
代码如下:
class MergeSort{
public static void mergeSort(int[] arr,int left,int right){
if(left>=right){
return;
}
//首先计算出中间位置 mid
int mid=(left+right)/2;
//然后递归调用 mergeSort 对左部分进行排序
mergeSort(arr,left,mid);
//然后递归调用 mergeSort 对右部分进行排序
mergeSort(arr,mid+1,right);
//最后调用 merge 方法将左右两部分合并
merge(arr,left,mid,right);
}
public static void merge(int[] arr,int left,int mid,int right){
//求两个临时数组 L 和 R 的长度,我们把mid的值分到左边的数组中
int len1=mid-left+1;
int len2=right-mid;
//两个临时数组 L 和 R 来存储左右两部分的数据
int[] L=new int[len1];
int[] R=new int[len2];
for(int i=0;i<len1;i++){
L[i]=arr[i];
}
for(int j=0;j<len2;j++){
R[j]=arr[mid+j+1];
}
int i=0,j=0;
//我们现在要把两个临时数组的数据排序放到arr中,index记录arr数组的下标
int index=left;
//逐个比较 L 和 R 中的元素,将较小的元素放入原数组arr中,直到其中一个数组遍历完毕
while (i<len1&&j<len2){
if(L[i]<R[j]){
arr[index]=L[i];
i++;
}else {
arr[index]=R[j];
j++;
}
//存好一个后,index++,继续存放下一个元素
index++;
}
//如果L数组未遍历完毕,继续遍历
while (i<len1){
arr[index]=L[i];
i++;
index++;
}
//如果R数组未遍历完毕,继续遍历
while (j<len2){
arr[index]=R[j];
j++;
index++;
}
}
}
归并排序总结
1. 归并的缺点在于需要O(N)的空间复杂度归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
五.排序算法复杂度及稳定性分析
那么到这里,我们常见的排序方式就总结完了
如果哪里有问题,欢迎在评论区询问或私聊我
最后,你的点赞收藏和关注就是对我最大的鼓励~~