常见的排序算法

常见的排序算法|

排序方法 平均时间 最好时间 最坏时间
冒泡排序(稳定 ) O(n2 ) O(n) O(n2)
选择排序(不稳定) O(n2 ) O(n2 ) O(n2 )
插入排序(稳定) O(n2 ) O(n) O(n2)
希尔排序(不稳定) O(n1.5)
快速排序(不稳定) O(nlogn) O(nlogn) O(n^2)
归并排序(稳定) O(nlogn) O(nlogn) O(nlogn)
堆排序(不稳定) O(nlogn) O(nlogn) O(nlogn)
基数排序(稳定) O(n) O(n) O(n)

关于空间复杂度:冒泡、选择、插入、希尔空间复杂度为O(1)
快排空间复杂度为logn(递归)
基数排序、归并排序空间复杂度为O(n)

冒泡排序

基本思想:俩个数比较大小,大的下沉,小的上浮
冒泡排序的过程想必大家都很清楚了,直接上代码
可食用代码—java版

private static void BubbleSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = arr.length - 1 - i; j > 0; j--) {
                if (arr[j] < arr[j - 1]) {
                    int tem = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tem;
                }
            }
        }
    }

上面就是冒泡排序最简单的做法,但是如果中间某一时刻已经有序了,该算法仍然会继续进行直到比较length次停止。
优化:设置标记flag,如果发生交换则设为true,否则排序结束

private static void BubbleSort_Better(int[] arr) {
        boolean flag;
        for (int i = 0; i < arr.length - 1; i++) {
            flag = false;//初始化标记
            for (int j = arr.length - 1 - i; j > 0; j--) {
                if (arr[j] < arr[j - 1]) {
                    int tem = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tem;
                    flag = true;
                }
            }
            if (!flag) {
                break;
            }
        }
    }

适用场景:较少元素时

选择排序

基本思想
在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
第二次遍历n-2个数,找到最小的数值与第二个元素交换;
。。。
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。

就是每次找第i小的并交换,直接上代码。

private static void SelctionSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            int min = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[min]) {
                    min = j;
                }
            }
            if (min != i) {
                int tem = arr[min];
                arr[min] = arr[i];
                arr[i] = tem;
            }
        }
    }

简单插入排序

基本思想:在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

代码如下:

private static void insertionSort(int[] arr) {

        for (int i = 1; i < arr.length; i++) {
            int tem = arr[i];
            int j = i - 1;
            while (j >= 0 && arr[j] > tem) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j+1] = tem;
        }
    }

优化:因为要插入的部分是有序的,所以我们可以用二分查找来缩短寻找插入位置时的时间(二分插入排序),但是主要的时间用在了复制操作上,所以时间上提升有限
适用场景:当数据基本有序时,插入排序可以接近O(n)的时间复杂度

希尔排序

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本,它与插入排序的不同之处在于,它会优先比较距离较远的元素,该方法因D.L.Shell于1959年提出而得名。希尔排序的整体思想是将固定间隔的几个元素之间排序,然后再缩小这个间隔。这样到最后数列就成为了基本有序数列,而前面我们讲过插入排序对基本有序数列排序效果较好。
具体步骤如下:

1、计算一个增量(间隔)值

2、对元素进行增量元素进行比较,比如增量值为7,那么就对0,7,14,21…个元素进行插入排序

3、然后对1,8,15…进行排序,依次递增进行排序

4、所有元素排序完后,缩小增量比如为3,然后又重复上述第2,3步

5、最后缩小增量至1时,数列已经基本有序,最后一遍普通插入即可

已知的最增量式是由 Sedgewick 提出的 (1, 5, 19, 41, 109,…),该步长的项来自 9 4^i - 9 2^i + 1 和 4^i - 3 2^i + 1 这两个算式。这项研究也表明 “比较在希尔排序中是最主要的操作,而不是交换。 用这样增量式的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比*快速排序慢
常见的排序算法

private static void shellSort(int[] arr) {
        int len = arr.length;
        //首先分组,分组长度从数组长度的一半开始,即选定增量
        for (int gap = len / 2; gap > 0; gap++) {
            //对每一组进行插入排序
            for (int i = gap; i < len; i += gap) {
                insert(arr, gap, i);
            }
        }
    }

    private static void insert(int[] arr, int gap, int begin) {
    	//和普通的插入排序原理相同
        int tem = arr[begin];
        int j = begin- gap;
        while (j >= 0 && tem < arr[j]) {
            arr[j + gap] = arr[j];
            j -= gap;
        }
        arr[j + gap] = tem;
    }

快速排序

这是一个经常用到的排序算法,java.util.Arrays中的sort()方法用的就是快排,当然我们这里说的经典快排,而sort()方法用的是双基准快排(比快排效率更高,但还是分治思想)。有兴趣的读者可以自行了解《Dual-Pivot QuickSort》。
经典快排思想

  • 先从数列中取出一个数作为key值;
  • 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
  • 对左右两个小数列重复第二步,直至各区间只有1个数。

辅助理解:挖坑填数

  • 初始时 i = 0; j = 9; key=72
    由于已经将a[0]中的数保存到key中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
    从j开始向前找一个比key小的数。当j=8,符合条件,a[0] = a[8] ; i++ ; 将a[8]挖出再填到上一个坑a[0]中。
    这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。
    这次从i开始向后找一个大于key的数,当i=3,符合条件,a[8] = a[3] ; j-- ; 将a[3]挖出再填到上一个坑中。
> 数组:72 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 48 - 85
>  		0    1    2     3     4     5     6     7     8     9
  • 此时 i = 3; j = 7; key=72
    再重复上面的步骤,先从后向前找,再从前向后找。
    从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
    从i开始向后找,当i=5时,由于i==j退出。
    此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将key填入a[5]。
数组:48 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 88 - 85
 	   0   1   2    3    4    5    6    7    8    9
  • 可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。
 数组:48 - 6 - 57 - 42 - 60 - 72 - 83 - 73 - 88 - 85
        0   1   2    3    4    5    6    7    8    9

因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。

代码如下:

 private static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int mid = getIndex(arr, low, high);
            quickSort(arr, low, mid - 1);//左半部分
            quickSort(arr, mid + 1, high);//右半部分
        }
    }

    private static int getIndex(int[] arr, int low, int high) {

        int tem = arr[low];//基准
        while (low < high) {
			//找第一个小于基准的数
            while (low < high && arr[high] >= tem) {
                high--;
            }
            arr[low] = arr[high];
			//找第一个大于基准的数
            while (low < high && arr[low] <= tem) {
                low++;
            }
            arr[high] = arr[low];
        }
        arr[low] = tem;
        return low;
    }

基准的选择可以有多种方式,如中间数或随机数,选择不同基准会影响算法时间
适用场景:在大规模数据集上性能好
缺点:在小规模数据集上表现较差
解决方案:先使用快排对数据集排序,使其达到基本有序;当数据规模达到一定小时,采用插入排序。

归并排序

基本思想归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序适用于子序列有序的数据排序。归并排序是分治法的典型应用。分治法(Divide-and-Conquer):将原问题划分成 n 个规模较小而结构与原问题相似的子问题;递归地解决这些问题,然后再合并其结果,就得到原问题的解。

常见的排序算法
代码如下:

private static void divide(int[] arr, int low, int high, int[] tem) {
		//此处划分数组
        if (low < high) {
            int mid = (high - low) >> 1 + low;
            divide(arr, low, mid, tem);
            divide(arr, mid + 1, high, tem);
            mergeSort(arr, low, mid, high, tem);
        }
    }

    private static void mergeSort(int[] arr, int low, int mid, int high, 		  int[] tem) {
    //合并数组
        int i, j, k;
        i = low;
        j = mid + 1;
        k = 0;
        while (i <= mid && j <= high) {
            if (arr[i] < arr[j]) {
                tem[k++] = arr[i++];
            }else {
                tem[k++] = arr[j++];
            }
        }

        while (i <= mid) {
            tem[k++] = arr[i++];
        }
        while (j <= high) {
            tem[k++] = arr[j++];
        }
        for (int t = 0; t < k; t++) {
            arr[low + t] = tem[t];
        }
    }

堆排序

首先堆可以分为大根堆和小根堆,从数据机构上来看是一个完全二叉树。所以根据这个特性,我们可以知道 i 节点的左孩子节点为2i+1,右孩子节点为2i+2.
基本思想:(大根堆)
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组

代码主要分为两步:
1、构建堆
2、将堆排序,将根节点和最后一个节点调换,然后重新建造堆
代码如下:

private static void heapsort(int[] arr) {

        //构建堆,从第一个非叶子节点开始建堆
        int len = arr.length - 1;
        int begin = len >> 1;
        for (int i = begin; i >= 0; i--) {
            maxHeap(arr, i);
        }

        //将根节点和最后一个元素互换,重建堆
        for (int i = len - 1; i > 0; i--) {
            swap(arr, 0, i);
            maxHeap(arr, i - 1);
        }
    }

    private static void maxHeap(int[] arr, int root) {
        int len = arr.length - 1;
        int le = root << 1 + 1;
        int ri = le + 1;
        int max = le;
        
        if(le > root) return;
        if (ri <= len && arr[ri] > arr[le]) { //判断左右节点大小
            max = ri;
        }
        if (arr[max] > arr[root]) {//父节点是否大于所有子节点
            swap(arr, max, root);
            maxHeap(arr,max);
        }
    }

    private static void swap(int[] arr, int a, int b) {
        int tem = arr[a];
        arr[a] = arr[b];
        arr[b] = tem;
    }

基数排序

因为基数排序是桶排序的扩展,所以先提一下桶排序。
桶排序:非常简单的思想,建立一个数组A[MaxValue],然后将每个数都放置到相应位置(如4放到下标为4的地方),之后遍历一遍数组,即可获得排序结果。
问题:如果maxValue很大,造成空间浪费
基数排序以桶排序为基础,通过限制基数来减少空间开销。
**基数排序思想:**先排好各位,然后排好各位的基础上排十位,以此类推,直到遍历最高位 次,排序结束(仔细理解最后一句话)
贴一张动图来帮助大家理解:
常见的排序算法
基数排序有两种排序方式:LSD和MSD,最小位优先(从右边开始)和最大位优先(从左边开始)
我下面的代码是LSD
可食用代码如下

private static void radixSort(int[] arr) {
        int wid = getWidth(arr);
        int weight = 1;//表示现在是个位
        int loc = 1;
        int k = 0;
        int len = arr.length;

        int[][] bucket = new int[10][len];
        int[] count = new int[10];//记录每个桶里的元素个数
        Arrays.fill(count, 0);
        while (loc <= wid) {

            for (int num : arr) {//将所有元素放入桶中
                int digit = (num / weight) % 10;
                bucket[digit][count[digit]] = num;
                count[digit]++;
            }

            for (int i = 0; i < 10; i++) {//将桶中的元素拷贝回原来数组中
                if (count[i] != 0) {
                    for (int j = 0; j < count[i]; j++) {
                        arr[k++] = bucket[i][j];
                    }
                }
                count[i] = 0;
            }
            k = 0;
            weight *= 10;
            loc++;
        }

    }

    private static int getWidth(int[] arr) {//获取最大值的位数
        int max = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        int wid = 0;
        while (max > 0) {
            wid /= 10;
            wid++;
        }
        return wid;
    }

参考资料:
基数排序–讲的很棒
排序算法总结—菜鸟教程
堆排序—简书
各种排序算法详解及优劣对比

常见的排序算法常见的排序算法 machine_Heaven 发布了10 篇原创文章 · 获赞 0 · 访问量 113 私信 关注
上一篇:八大内部排序


下一篇:【学习笔记】C#图片压缩(好用)