基础排序算法的实现,时间复杂度,空间复杂度,是否稳定

排序算法

本篇文章介绍:选择,冒泡,插入,归并,快排,堆,桶(计数,基数排序),一共8排序方法。
稳定性概念的解释:经过排序之后,同样大小的样本相对次序不变化
对于基础数据类型来说稳定性无意义,但是引用类型来说稳定性很重要

1.选择排序

思路:[i,length-1]上选择最小的值,交换i-1和最小值的,1=<i<length
public static void selectionSort(int[] arr){
        if(arr == null || arr.length<=0){
            return;
        }
        for (int i = 0; i < arr.length-1; i++) {
            int minIndex = i;
            for (int j = i+1; j < arr.length; j++) {
                minIndex = arr[j]<arr[minIndex]?j:minIndex;
            }
            swap(arr,i,minIndex);
        }
    }
	// 交换
    private static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

时间复杂度:O(n^2),空间复杂度:O(1),稳定性:无
无稳定性是因为:swap的时候有可能跨度很大

2.冒泡排序

思路:从左到右两两比较最大的放在最后面
private static void bubbleSort(int[] arr){
        if(arr == null || arr.length<2 ){
            return;
        }
        for (int i = arr.length-1; i > 0; i--) {
            for (int j = 0; j < i; j++) {
                if (arr[j] > arr[j+1]) {
                    swap(arr,j,j+1);
                }
            }
        }
    }

    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(1),稳定性有

3.插入排序

思路:0~0、0~1...0~n上保持有序
public static void insertionSort(int[] arr){
        if(arr == null || arr.length <2){
            return;
        }
        for (int i = 1; i < arr.length; i++) {
            for (int j = i-1; j >=0 && arr[j+1]<arr[j] ; j--) {
                swap(arr,j,j+1);
            }
        }
    }

    private static void swap(int[] arr, int i, int j) {
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

时间复杂度:O(n^2),空间复杂度O(1),稳定性有

4.归并排序

思路:递归+合并(将数组不断折半拆分,直到拆分的左数组和右数组是有序的,也就是分别是单个元素的时候,将递归的当前层的左右两个数组进行合并的过程)

private static void mergeSort(int[] arr){
        if(arr == null || arr.length <=1){
            return;
        }

        process(arr,0,arr.length-1);
    }

    private static void process(int[] arr, int l, int r) {
        if(l == r){
            return;
        }
        int mid = l+((r-l) >> 1);
        process(arr, l, mid);
        process(arr, mid+1, r);
        merge(arr,l,mid,r);
    }

    private static void merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1];
        int i = 0;
        int p1 = l;
        int p2 = mid + 1;
        while (p1 <= mid && p2 <= r) {
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[l + i] = help[i];
        }
    }

时间复杂度:O(n*log(n)),空间复杂度:O(n)因为merge的过程需要等长的数组辅助做数据的迁移,稳定性:有

5.随机快排

思路:荷兰国旗问题,随机选一个数组中的值,将和这个值相等的数值的值的位置搞定,然后在搞定左右两个数组。

public static void quickSort(int[] arr){
        if(arr == null || arr.length<2){
            return;
        }
        process(arr,0,arr.length-1);
    }

    private static void process(int[] arr, int l, int r) {
        if(l >= r){
            return;
        }

        // 优化的话,使用随机数,取r
        swap(arr,l+ (int)(Math.random()*(r-l+1)),r);

        int[] mids = partition(arr,l,r);//选择r索引的数据进行快速排序
        process(arr,l,mids[0]-1);
        process(arr,mids[1]+1,r);
    }

    private static int[] partition(int[] arr, int l, int r) {
        if(l>r){
            return new int[]{-1,-1};
        }
        if(l==r){
            return new int[]{l,l};
        }
        int p1 = l - 1;
        int p2 = r;
        int index = l;
        while(index<p2){
            if(arr[index]<arr[r]){
                swap(arr,index++,++p1);
            }else if(arr[index] > arr[r]){
                swap(arr,index,--p2);
            }else{
                index++;
            }
        }
        swap(arr, p2, r);
        return new int[]{p1+1,p2};
    }

上诉代码parttition的过程选择的是所以为right的数据,进行partition,随机快排具有不稳定性
时间复杂度:O(N*log(N)),空间复杂度:O(log(N)),都是期望值,稳定性无法保证,在荷兰国旗分割的时候,会有swap,可能跨度很大,无法保证稳定性。

6.堆排序

堆结构:数组实现的完全二叉树
最小堆:最小值在上面,且nodes[i]<= nodes[2i+1]&& nodes[i]<=nodes[2i+2]
最大堆:最大值在上面,且nodes[i]>=nodes[2i+1]&& nodes[i]>= nodes[2i+2]

首先手动实现一个最大堆:

		private int[] heap;
        private final int limit;
        private int heapSize;
		
		// init
        public MyMaxHeap(int limit) {
            heap = new int[limit];
            this.limit = limit;
            heapSize = 0;
        }

        // pop之后需要维护heapify
        public int pop() {
            int maxInt = heap[0];
            swap(heap, 0, --heapSize);
            heapify(heap, 0, heapSize);
            return maxInt;
        }

        private void heapify(int[] heap, int index, int heapSize) {
            int left = 2 * index + 1;
            while (left < heapSize) {
                int large = left + 1 < heapSize && heap[left] < heap[left + 1] ? left + 1 : left;
                large = heap[index] < heap[large] ? large : index;
                if (large == index) break;
                swap(heap, index, large);
                index = large;
                left = 2 * index + 1;
            }
        }

        // 插入数据的时候,首先将数据插入数组中,并且通过heapInsert保障大根堆的数据结构
        public void push(int value) {
            if (isFull()) {
                throw new RuntimeException("heap is full");
            }
            heap[heapSize] = value;
            // 从下向上一次跟父进行比较,如果大于,交换,保证大根堆
            heapInsert(heap, heapSize++);
        }

        private void heapInsert(int[] arr, int index) {
            while (arr[index] > arr[(index - 1) / 2]) {
                swap(arr, index, (index - 1) / 2);
                index = (index - 1) / 2;
            }
        }

        private void swap(int[] arr, int i, int j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }

        public Boolean isEmpty() {
            return heapSize == 0;
        }

        public Boolean isFull() {
            return heapSize == limit;
        }

堆排序的代码:主要就是依靠heapInsert维护一个最大堆,将最大值(index:0) 与index:–size进行swap,就搞定了一个元素,然后在不断循环直到size为0

public static void heapSort(int[] arr) {
        if(arr == null || arr.length<2){
            return;
        }
        //对于已经给顶arr,首先新城大根堆的结构 时间复杂度位O(n*logn)
        for (int i = 0; i < arr.length; i++) {
            heapInsert(arr,i);
        }
        int size = arr.length;
        swap(arr,0,--size);
        while(size>0){
            heapify(arr,0,size);//时间复杂度为0(logn )
            swap(arr,0,--size);
        }
    }

    private static void heapify(int[] arr, int index, int size) {
        int left = 2*index+1;
        while(left<size){
            int lager = (left+1)<size && arr[left+1]>arr[left]?left+1:left;
            lager = arr[index]>arr[lager]?index:lager;
            if(lager == index){
                break;
            }
            swap(arr,index,lager);
            index = lager;
            left = 2*index +1;
        }
    }

    // 单次的时间复杂度位O(logN)
    private static void heapInsert(int[] arr, int index) {
        while(arr[index] > arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index = (index-1)/2;
        }
    }

    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

时间复杂度:O(N*log(N)),空间复杂度O(1),稳定性:无,因为在维护堆结构的时候,父子节点进行交换的时候跨度太大,会破坏稳定性

7.计数排序

一般来讲,计数排序要求,样本是整数,且范围比较窄
比如按照年龄进行排序,时间复杂度可以做到O(n),额外的空间复杂度:O(M)

public static void countSort(int[] arr){
        if(arr == null || arr.length<2){
            return;
        }
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max,arr[i]);
        }
        int[] help = new int[max+1];
        for (int i = 0; i < arr.length; i++) {
            help[arr[i]]++;
        }
        int index = 0;
        for (int i = 0; i < help.length; i++) {
            while(help[i]-- > 0){
                arr[index++] = i;
            }
        }
    }

时间复杂度:O(N),空间复杂度O(M),稳定性:有,因为没有对数组进行交换,所以可以维持原数组的稳定性

8.基数排序

一般来讲,基数排序要求,样本是10进制的正整数

思路:数组中最大数,拿到其位数,在不同位上进行count++计数,然后将按照相同位的大小进行重新排序,然后在高位上进行重复操作

	public static void radixSort(int[] arr) {
        if(arr == null || arr.length<2){
            return;
        }
        int max= Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max,arr[i]);
        }
        radixSort(arr,0,arr.length-1,maxbit(max));
    }

    private static int maxbit(int x) {
        int result =0;
        while(x!=0){
            x=x/10;
            result++;
        }
        return result;
    }
	public static void radixSort(int[] arr,int l,int r,int bit){
    	// 辅助空间
        int[] help = new int[r-l+1];
        for (int i = 1; i <= bit; i++) {
       		// bit上的数值0~9
            int[] count = new int[10];
            for (int j = l; j <= r ; j++) {
            	// 获取获取数值上的i位的值
                int digit = getDigit(arr[j],i);
                count[digit]++;
            }
            // 统计小于等于当前位数的个数
            for (int j = 1; j < count.length; j++) {
                count[j] = count[j] + count[j-1];
            }
            // 从右向左依次放置
            for (int j = r; j >= l; j--) {
                int digit = getDigit(arr[j], i);
                help[--count[digit]] = arr[j];
            }
            // 重新赋值给arr数组
            for (int j = 0; j < arr.length; j++) {
                arr[j] = help[j];
            }
        } 
    }
    //获取位数对应的数字
    private static int getDigit(int num,int bit) {
        return (num/(int)Math.pow(10,bit-1)) % 10;
    }

时间复杂度O(n),空间复杂度O(N),稳定性:有
基数排序也交桶排序,先进后出,我们从左向右依次放入桶中,取出的时候从右向左依次赋值,就不会破坏稳定性。

总结

一张图说明一切:(图的来源是:左程云老师)
在这里插入图片描述

1.根据上诉内容,看你在排序的时候,结合实际的业务需求选择不同的排序方法,有一点比较重要的是对于归并,随机,堆排序,如果不考虑稳定,不考虑空间复杂度的话,选择随机快排,因为快排的时间复杂度参数项小。

2.还有一点需要注意的是,上诉排序算法可以结合在一起使用,组合排序,按照样本的类型或者样本的数量进行划分,在不同类型和数量上采用不同的算法,这样整体的时间复杂度是最优的,eg:Arrays.sort();

这个就是基础排序部分的内容总结,排序每次看的时候都很理解,但是每次去写的时候总归会有问题产生,原因就是因为不熟练,写这个文章也是自己的总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值