七种常见排序的实现和比较

本文详细介绍了排序算法中的七种常见方法:插入排序、冒泡排序、快速排序、归并排序、堆排序、直接选择排序和希尔排序。分别阐述了它们的思想、实现代码、时间复杂度、空间复杂度以及稳定性。其中,冒泡、选择和归并排序是稳定的,而快速、堆和希尔排序则不是。在时间复杂度上,快速、归并、堆排序平均为O(nlogn),其余为O(n²)。空间复杂度方面,归并排序需要额外空间,而其他算法则不需要或只需常数空间。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. 七种常见的排序

1.插入排序

思想:将数组拆分成两段,一段有序,一段无序,将无序区间中的数依次拆入到有序区间中。示例图:
在这里插入图片描述
实现代码

public void insertSort(int[] array) {
        //i 循环控制插入次数及要插入的数
        for(int i = 0;i < array.length-1; i++) {
            //temp = 要拆入的数,从下标为1的位置开始是因为一个数的时候自然有序
            int temp = array[i+1];
            //在有序数组中找出要插入的位置(从后往前遍历有利于对有序数组进行插入操作)
            int j = i;
            for (; j >= 0; j--) {
                //如果要插入的数小于当前数 令当前数后移一位 否则退出循环(既找到了要拆入的位置为array[j]的后一位)
                if(temp < array[j]) {
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            //将数插入
            array[j + 1] = temp;
        }
    }

时间和空间复杂度及稳定性:
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:因为后一个数要是和前面的相等之间插入前一个数后面,所以具有稳定性

2.冒泡排序

思想:从一个数开始比较找到最大的放到数组的最后一个位置,在对剩下的数继续比较将最大的数放在倒数第二位,依次类推直到全部有序。示例图:
在这里插入图片描述
然后在对蓝色区间进行上述操作:
在这里插入图片描述
直到数组有序

实现代码

public void bubbleSort(int[] array) {
        //i循环控制循环次数
        for (int i = 0; i < array.length; i++) {
            //每次j循环找出最大的数放在最后面,下次循环可以少比较一次
            for (int j = 0; j < array.length - i - 1; j++) {
                //比较两个数,将大的数放在后面,以保证可以把最大的数放在最后一个位置
                if(array[j+1] < array[j]) {
                    int t = array[j+1];
                    array[j+1] = array[j];
                    array[j] = t;
                }
            }
        }
    }

时间复杂度,空间复杂度和稳定性
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:因为只有大小不等的时候才进行交换,所以具有稳定性

3.快速排序

思想:找到一个数将数组分为两部分,左边小于这个数,右边大于这个数,(有点像搜索树),然后对左右两边的得到的数组继续进行拆分,直到数字中只有一个数或者没有数(分而治之)。示例图:
在这里插入图片描述
此时数组类的数所在的位置就是图中的的关系(树的关系)

实现代码

    public void quickSort(int[] array) {
        quickSortInternal(array,0,array.length-1);
    }
    //这个方法是为了递归实现更换了原方法的参数列表
    public void quickSortInternal(int[] array, int lowIndex, int highIndex) {
        //如果数组长度 <= 1 自然有序
        //因为时左闭右闭 所以数组长度为 rightIndex - leftIndex + 1
        if(highIndex - lowIndex + 1 <= 1) {
            return;
        }
        //找到中间位置的下标
        int midIndex = partition(array,lowIndex,highIndex);
        //对数组左边再次进行快排([lowIndex,midIndex-1])
        quickSortInternal(array,lowIndex,midIndex-1);
        //对数组右边再次进行快排([midIndex+1,highIndex])
        quickSortInternal(array,midIndex +1,highIndex);
    }
    public int partition(int[] array,int lowIndex,int highIndex){
        //令第一个数为中间数(这里为了方便实现,实际中可以对要选的中间数进行优化)
        int key = array[lowIndex];
        int leftIndex = lowIndex;
        int rigthIndex = highIndex;

        while(leftIndex < rigthIndex) {
            //找到右边小于中间数 的数
            while(leftIndex < rigthIndex && array[rigthIndex] >= key) {
                rigthIndex--;
            }
            //找到左边大于中间数 的数
            while(leftIndex < rigthIndex && array[leftIndex] <= key) {
                leftIndex++;
            }
            //交换这两个数
            int temp = array[leftIndex];
            array[leftIndex] = array[rigthIndex];
            array[rigthIndex] = temp;
        }
        //到这里时除了第一个数(确定的中间数)其他数都有序了,且leftIndex=rightIndex 为小于或等于中间数的位置下标
        //将第一个数和下表为leftIndex位置的数进行交换
        int temp = array[lowIndex];
        array[lowIndex] = array[leftIndex];
        array[leftIndex] = temp;
        //返回中间下标
        return leftIndex;
    }

时间复杂度,空间复杂度,稳定性:
时间复杂度:每次都要划分数组划分次数为logn(搜索树的高度),每次都要进行遍历拆分的数组,数组内元素个数为n,因此时间复杂度为:O(nlogn)
空间复杂度:开辟了常数个单元(拆分的时候开辟的单元),所以空间复杂度为:O(logn)
稳定性:因为要和前面的数进行交换,两个数一样的话会改变位置,所以不具备稳定性

4.归并排序
思想:将数组从中间拆分为两个数组,分别使这两个数组有序,然后继续拆分,直到不能在拆分了合并两个数组,依次合并两个有序数组,直到整个数组有序(先拆再和)。示例图:
黑色为“拆”的过程,红色为“合”的过程
实现代码

    public void mergeSort(int[] array) {
        mergeSortInternal(array,0,array.length);
    }
    //这个方法是为了递归实现更换了原方法的参数列表
    public void mergeSortInternal(int[] array,int leftIdnex,int rightIndex) {
        //如果数组长度 <= 1 既数组中只右一个数 自然有序
        //因为时左闭右开 所以数组长度为 rightIndex - leftIndex
        if(rightIndex - leftIdnex <= 1) {
            return ;
        }
        //找到要进行拆分的位置
        int midIndex = (rightIndex + leftIdnex)/2;
        //对左边进行归并
        mergeSortInternal(array,leftIdnex,midIndex);
        //对右边进行归并
        mergeSortInternal(array,midIndex,rightIndex);
        //合并两个有序区间
        merge(array,leftIdnex,midIndex,rightIndex);
    }
    public void merge(int[] array,int leftIndex,int midIndex,int rightIndex) {
        //定义一个需要合并的临时数组
        int[] mergeArray = new int[rightIndex - leftIndex];
        int i = leftIndex;
        int j = midIndex;
        int t = 0;
        //当两个数组都没有遍历完时,对他们进行进行合并(放到临时数组中)
        while(i < midIndex && j < rightIndex) {
            if(array[i] <= array[j]) {
                mergeArray[t++] = array[i++];
            }else {
                mergeArray[t++] = array[j++];
            }
        }
        //有一个数组以及遍历完,将剩下的数放入临时数组中
        while(i < midIndex) {
            mergeArray[t++] = array[i++];
        }
        while(j < rightIndex) {
            mergeArray[t++] = array[j++];
        }
        //把临时数组中的数按位置搬到原数组中
        for (int value : mergeArray) {
            array[leftIndex++] = value;
        }
    }

时间复杂度,空间复杂度和稳定性
时间复杂度:要先拆,且是从中间拆,要拆logn次,每次合并都要遍历数组,所以时间复杂度是O(nlongn)
空间复杂度:每次合并都要开辟新的空间,合并log2n次,每次拆分新的数组都会利用一个空间,所以要用n个空间,所以空间复杂度为:O(n)
稳定性:因为交换元素时,可以在相等的情况下做出不移动的限制,所以归并排序是可以稳定

5.堆排序
思想:将数组构建成一个小堆,每次取出堆顶元素,然后把最后面的元素放的堆顶,进行向下调整
实现代码:参考java 的优先级队列(priorityQueue)
时间复杂度,空间复杂度,稳定性
时间复杂度:因为是堆的数据结构实现,所以时间复杂度为O(nlogn)
空间复杂度:因为是就地排序不开辟新的空间所以空间复杂度为:O(1)
稳定性:建队过程破坏数组顺序,所以不稳定

6.直接选择排序:
思想:从数组中找出最小的,放在第一位,然后从剩下的数据中再找出最小的放到第二位依次类推
实现代码

public void SelectSort(int[] arr){
        for(int i = 0;i<arr.length-1;i++){
            for(int j = i+1;j<arr.length;j++){
                if(arr[i] <= arr[j])
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
            }
        }
    }

时间复杂度,空间复杂度和稳定性:
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:因为是选择最小的或者等于依次放在前面,会改变相等的元素的位置关系,所以不稳定

7.希尔排序
思路:根据增量序列的值ti,每趟排序会把初始序列划分成若干个元素的子序列,然后对这些子序列使用插入排序,因为这是递减增量序列,所以第一趟的排序,增量值最大,那么划分的子序列数量也就最多,每个子序列的元素也就越少,可以看做是一个“几乎”已经排好序的序列,当增量值越来越小的时候,子序列数量也就越少,每个子序列的元素也就越多,但是,基于前几次的排序,这些子序列虽然元素多,但是已经是一个“几乎”排好序的序列了,当最后一次排序的时候,即增量序列的值为1时,就是对整个无序序列进行排序,这时整个序列已经是一个“几乎”排好序的序列了。
实现代码

public void shellSort(int[] arr) {
		if (arr == null || arr.length == 0) {
			return;
		}
		int len = arr.length;
		// 首先选取一个增量序列
		int gap = 1;
		while (gap < len) {
			// 先找出增量序列最大的增量值,也就是将序列分为gap组
			gap = gap * 3 + 1;
		}
        while (gap > 0) {
            for (int i = gap; i < len; i++) {
                int tmp = arr[i];
                int j = i - gap;
                while (j >= 0 && arr[j] > tmp) {
                    arr[j + gap] = arr[j];
                    j -= gap;
                }
                arr[j + gap] = tmp;
            }
            gap = (int) Math.floor(gap / 3);
        }

时间复杂度,空间复杂度,稳定性
时间复杂度:效率依赖于递减增量序列的选择,时间复杂度最坏的情况是O(nlogn)
空间复杂度:没有开辟新空间(开辟了常数个空间),所以空间复杂度为:O(1)
稳定性:因为要选择比较区间会破坏稳定性,所以不稳定性

表格总结
在这里插入图片描述
具有稳定性的排序:冒泡,选择,归并
平均时间复杂度为O(nlogn):希尔,快排,归并,堆排
空间复杂度不为O(1):快排(O(logn)),归并(O(n))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值