排序算法快速入门!(对小白很友好)

首先需要明确的是,我们说的排序默认就是从小到大的,也即升序

        时间复杂度是衡量算法流程的复杂程度,只与数据量有关,与过程之外的优化无关。此外,还有常数时间:如果一个操作不与具体的数据量有关,那就是常数时间,即O(1)。比如说HashMap你可能存了几万个数,但我只查某一个桶上的位置,这个桶上的数据通常就只有有限的几个,查几次也好 几十次也好 通常都认为是O(1)的操作,因此我们说哈希表的时间复杂度是大常数

        纯小白可以参考(并不严谨,但对于快速入门应该能起到一定的帮助),可以粗浅地理解为for(i....N)就是O(N),两个非嵌套的for(i....N)总体还是O(N),但如果是嵌套的两个for(i....N)就是O(N^2)。        

        空间复杂度的话,有限几个变量就是O(1),如果需要利用额外的数组进行辅助,就是O(N)。注意,可以认为答案所需要返回的数组通常不算在空间复杂度内。(仍然仅供参考)

       我认为所谓的最优解大概就是在保证时间复杂度尽可能低的情况下,使用尽量少的空间完成。

       接下来就是排序算法的稳定性:这里的稳定性并不是说时间复杂度忽快忽慢,因为时间复杂度都是按最差的情况来算的,没有所谓的时间复杂度忽快忽慢。稳定性简单说来就是:相同的值,你排完后次序是否会发生变化。

        举例:经过某次排序后的情况是y1 y2  y3,如果无论怎么排你都是这个相对次序,也即y1在y2之前,y2在y3之前,那么就是稳定的排序算法。

        对于基本数据类型来说,算法排序的稳定性没什么意义。本来你基本数据类型也没区分,都是字面量。何谈什么相对次序呢?

        对于引用数据类型就很有意义了。比如说你根据价格进行排序,然后你再根据评价排序,那么排在最前面的就是评价最高的,并且评价最好的商品中内部价格也是最便宜的。也即物美价廉

还需要强调的是,排序算法的稳定性是说其算法能够经过代码控制能够做到的,有些无论如何也无法改写成稳定性。如果就是故意乱写,那什么算法也无法做到稳定性的~

  • 选择排序——时间复杂度O(N^2),空间复杂度O(1),不稳定

        选择排序的基本思想是在未排序序列中选择最小,然后将其放到已排序序列的末尾。 可以代入一种边界的感觉,i之前的都是有序,从i+1往后找一个最小的,和i交换。然后扩大i,直到 i == N

        每次从i ~ N-1 中选择最小的数放到i号位置上。比如 0 ~ N选最小放到0号位置(交换),1 ~N选最小放到1号位置(交换)...

        选择排序是不稳定的,比如:3 3 2。根据选择排序算法,会将2和第一个3进行交换,那么第一个3就排在了第二个3后面,也即 2 3 3 .无法做到稳定性(再次强调,基本数据类型仍然没有所谓的稳定性,这里只是为了方便举例。可以把例子中的数字看成对象~)

//选择排序
class Solution{
    //排序无需返回值,Java引用传递
	public void selectionSort(int[] arr) {
		//如果发现数组为null,或者没有元素、或者只有一个
		if (arr == null || arr.length < 2) {
			//没必要排序,直接返回
			return;
		}
		int N = arr.length;
		for (int i = 0; i < N; i++) {  //最外层的for表示 i ~ N 范围依次变有序
			//假设当前i位置就是最小值的索引    
			int minIndex = i;
			//内层for表示:从i+1开始找    i+1 ~ N上的最小值的索引
			for (int j = i + 1; j < N; j++) {
				//只要发现有元素比我当前最小值还小,minIndex记录下来
				minIndex = arr[j] < arr[minIndex] ? j : minIndex;
			}
			//最后,i和minIndex上的数进行交换,表示0~i上已经变有序
			swap(arr, i, minIndex);
		}
	}
}
  • 冒泡排序——时间复杂度O(N^2),空间复杂度O(1),稳定

        如果说我们的选择排序是每次挑一个最小的放到前面来,那么冒泡排序就是每次挑一个最大的数放到后面去

        冒泡可以做到稳定性,两两相等的时候不让他们交换就行了。【两两相等不往后

        3231 ——》2331(相等,橙3不能再往后)——》2313 ——》 2133——》1233

        可以看到橙3始终在绿3之前。值得一提的是,我这里提供的代码也是稳定性的。只有之前的数严格大于我,才能交换~

//冒泡排序
class Solution{
	public static void bubbleSort(int[] arr){
		//排序操作默认的健壮性判断
		if (arr == null|| arr.length < 2){
			return;
		}
		//表示0 ~ N-1排序,0 ~ N-2上排序,0 ~ N-3上排序
		//【每次循环都会导致最大的数被交换到最后】
		for(int end = arr.length -1 ; end >= 0 ; end--) {
			for (int i = 1; i <= end; i++) {
				if (arr[i - 1] > arr[i]) {//严格大于才交换,保证稳定性
					swap(arr, i, i - 1);
				}
			}
		}
	}
}
  • 插入排序——时间复杂度O(N^2),空间复杂度O(1),稳定

        插入排序的基本思想是将一个元素插入到已经排好序的部分,逐步构建有序序列。       

        可理解为整理牌,来一个新的牌就往前找合适的位置放。也是稳定的。【两两相等不往前

//插入排序
class Solution{
	public void insertionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		//最外层for就表示将新的卡牌加入我们的队伍
		for (int i = 1; i < arr.length; i++) {
			//内层for表示它应该往前移动到哪?
			for (int j = i - 1; j >= 0; j--) {
				//之前的数严格大于我,才能前进。
				if(arr[j] > arr[j + 1]){//严格大于才交换,保证稳定性
					//swap(arr,j,j+1) 
					int tmp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = tmp;
				}
			}
		}

		/**
		关于这里的交换,还有一种装逼的写法。不过需要确保交换的两个位置不能是同一个索引。只要不是相同位置交换,都可以用。也就是说你不能0位置和0位置进行交换。由于这里的插入排序代码,保证了j和j+1不可能是同一个数组索引,因此可以使用这种方式
		public 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 * logN),空间复杂度O(N),稳定

        归并排序是一种分治算法,它将一个大的问题分解成小的子问题来解决,然后将子问题的解合并起来,最终得到整个问题的解。归并排序的基本思想是分而治之,主要的步骤是

  1. 拆分: 将待排序的数组分成两个长度相等的子数组。
  2. 递归: 递归地对两个子数组进行归并排序。
  3. 合并: 将已经排序的两个子数组合并成一个有序数组。      

        归并排序也可以做到稳定。当左组和右组都出现了相同的时候,优先拷贝左边的。保证左边的1不会跑到右边的1。左组:[1  1   2]  右组:[1  1  3]  ——》合并:[1 1 1 1 2 3]

//归并排序
class Solution{
	public void mergeSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		dfs(arr, 0, arr.length - 1);
	}
	//将arr[l..r]排序
	public void dfs(int[] arr, int L, int R) {
		if (L == R) { // base case
			return;
		}
		int mid = L + ((R - L) >> 1);
		dfs(arr, L, mid);
		dfs(arr, mid + 1, R);
		//merge前提:左右都有序了
		merge(arr, L, mid, R);
	}

	//合并过程需要辅助数组,那么就在merge过程(方法)中定义辅助数组
	public void merge(int[] arr, int L, int M, int R) {
		int[] help = new int[R - L + 1];//L到R上有多少个数,辅助数组就准备多大
		int i = 0;//为help数组准备的索引
		int p1 = L;//左半部分的第一个数
		int p2 = M + 1;//右半部分的第一个数
		//开始拷贝咯
		//当两边都未越界时
		while (p1 <= M && p2 <= R) {
			//【相等的时候,仍然要拷贝左边p1,保证稳定性】
			help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
		}
		// 要么p1越界了,要么p2越界了。必然会一个越界,一个没越界(规定相同就拷左边)
		// 下面两个while只会发生一个,谁没越界谁把它剩下的拷贝到help去
		while (p1 <= M) {
			help[i++] = arr[p1++];
		}
		while (p2 <= R) {
			help[i++] = arr[p2++];
		}

		//最后把help中的数拷贝到原数组中
		for (i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
	}
}
  • 随机快排——时间复杂度O(N * logN),空间复杂度O(logN),不稳定

        快速排序使用分治策略将一个数组分成两个子数组,然后递归地对子数组进行排序。在快速排序中,通常会选择一个基准元素,将小于基准的元素放在左边,大于基准的元素放在右边,然后对左右两个子数组分别进行递归排序。

        随机快排是快速排序的一种变体,它在选择基准元素时采用随机化的方式。这样可以降低快速排序在某些特定情况下的最坏情况时间复杂度,并提高算法的平均性能。

        随机快排并不是稳定的,因为分区(Partition)过程并不是稳定的。比如当前选的基准元素是3,那么对于4   4   2就会变成 2  4  4

//正宗的随机快排~~~
class Solution{
	public void quickSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		dfs(arr, 0, arr.length - 1);
	}
	public void dfs(int[] arr, int L, int R) {
		if (L >= R) {
			return;
		}
		//随便选一个数作为基准元素
		swap(arr,
				L + (int) (Math.random() * (R - L + 1)),
				R);
		//进行分区
		int[] equalArea = partition(arr, L, R);
		//子过程再次进行
		dfs(arr, L, equalArea[0] - 1);
		dfs(arr, equalArea[1] + 1, R);
	}

	public int[] partition(int[] arr, int L ,int R){
		if (L > R) { // L...R   居然出现了L>R,不是有效范围
			return new int[] { -1, -1 };
		}
		if (L == R) {
			return new int[] { L, R };
		}
		int less = L - 1; // < 区 右边界
		int more = R; // > 区 左边界
		int index = L;//遍历到的索引位置
		while (index < more) { // 当前位置,不能和 >区撞上
			//三种情况:
			//①如果当前数 == 目标,直接跳下一个
			if (arr[index] == arr[R]) {
				index++;

				//②当前数 小于 目标 ==》和小于区下一个交换,并跳下一个
			} else if (arr[index] < arr[R]) {
				swap(arr, index++, ++less);

				//③当前数 大于 目标 ==》 和大于区上一个交换,但 不动 当前数index!
			} else { // >
				swap(arr, index, --more);
			}
		}
		//跑完上面的while后,在L~R-1上已经做好划分了。此时 <R  =R  >R
		//还需要把R和大于区的第一个进行交换。因为是根据arr[R]进行划分的!
		swap(arr, more, R);
		//less是小于区的最后一个   因此less+1就是等于区的第一个
		//more位置原来是大于区第一个,但是经过了上面的swap,more位置上的数就变成了目标数。
		return new int[] { less + 1, more };
	}
}
  • 堆排序——时间复杂度O(N * logN),空间复杂度O(1),不稳定

        堆排序是一种基于二叉堆数据结构的排序算法。它的基本思想是将待排序的元素构建成一个二叉堆,然后依次取出堆顶的最大元素,将取出的元素放到已排序部分的末尾,再调整堆,直到所有元素都被取出。【大白话:先建堆(大根堆)——》和堆底交换(数组末尾)——》调堆】

        在建立大根堆时其实就不稳定了,可能会交换相等元素的相对位置。比如数组是[1, 1, 1,2] ,建堆会2和1交换。那么绿色的1就会跑到蓝色的1后面[1, 2, 1,1] 。具体的过程画一个二叉树就好理解了,作者还不知道怎么画图~~~

class Solution{
	public void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
		//【建堆过程二选一】
        // O(N*logN) ————这个是从上到下的建堆方式。如果是动态数据流(不断加入数组),用这种
        /*for (int i = 0; i < arr.length; i++) {
			heapInsert(arr, i);
		}*/
		
        // O(N) ————这个是从下到上的建堆方式。如果是静态数据流(一开始就有所有的数据),用这种
        for (int i = arr.length - 1; i >= 0; i--) {
            heapify(arr, i, arr.length);
        }
        //堆的大小是数组的大小
        int heapSize = arr.length;
        swap(arr, 0, --heapSize);
        // 整体时间复杂度 O(N*logN)
        while (heapSize > 0) { // O(N)
            //先调堆
            heapify(arr, 0, heapSize); // O(logN)
            //再下一轮交换,直至heapSize == 0
            swap(arr, 0, --heapSize); // O(1)
        }
    }

    // arr[index]刚来的数,往上
    public void heapInsert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    // arr[index]位置的数,能否往下移动
    public void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1; // 左儿子的下标
        while (left < heapSize) { // 下方还有儿子的时候
            // 两个儿子中,谁的值大,把下标给largest
            // 1)只有左儿子,left -> largest
            // 2) 同时有左儿子和右儿子,右儿子的值 <= 左儿子的值,left -> largest
            // 3) 同时有左儿子和右儿子并且右儿子的值 > 左儿子的值, right -> largest
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 父和较大的儿子之间,谁的值大,把下标给largest
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                // 儿子都没有我值大,不再往下
                break;
            }
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }

    public void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}
  • 计数排序——时间复杂度O(N),空间复杂度O(M),稳定

       计数排序是一种线性时间复杂度的稳定排序算法(非比较性),适用于排序一定范围内的整数。它的基本思想是统计每个元素的个数,然后根据统计信息将元素放回原数组的正确位置。

        空间复杂度中的 M 表示元素的取值范围,即不同元素的个数。

//计数排序
class Solution{
	public 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[] bucket = new int[max + 1];//bucket相当于是help辅助数组
		//桶里每个数的下标 对应 arr中会出现的数
		for (int i = 0; i < arr.length; i++) {
			//arr[i]的值在bucket桶里的个数再加1
			bucket[arr[i]]++;
		}
		int i = 0;
		for (int j = 0; j < bucket.length; j++) {
			while (bucket[j]-- > 0) {//如果桶这个位置上有值,证明是arr中出现的次数
				arr[i++] = j;
			}
		}
	}
}
  • 基数排序——时间复杂度O(N),空间复杂度O(N),稳定

       基数排序同样是一种非比较性的排序算法,它根据元素的位数来进行排序。

//基数排序
class Solution{
	public void radixSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		radixSort(arr, 0, arr.length - 1, maxbits(arr));
	}

	//先找出最大的数,然后看他有多少位数
	public int maxbits(int[] arr) {
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			max = Math.max(max, arr[i]);
		}
		int res = 0;//记录最大的位数
		while (max != 0) {
			max /= 10;
			res++;
		}
		return res;
	}

	// arr[L..R]排序  ,  最大值的十进制位数digit
	//指定L...R是为了灵活性更好、易扩展。而不是要递归
	public void radixSort(int[] arr, int L, int R, int digit) {
		int radix = 10;
		int[] help = new int[R - L + 1];
		int i = 0,j= 0;
		for (int d = 1; d <= digit; d++) {
			int[] count = new int[radix];
			for (i = L; i <=R ; i++) {
				j = getDigit(arr[i], d);
				count[j]++;
			}
			for (i = 1; i < count.length ; i++) {
				count[i] += count[i-1];
			}
			for(i = R; i >= L ; i--) {
				j = getDigit(arr[i], d);
				help[count[j] - 1] = arr[i];
				count[j]--;
			}
			for (i = L,j = 0; i <= R ; i++,j++) {
				arr[i] = help[j];
			}
		}

	}

	public int getDigit(int x, int d) {
		return (x / (int) Math.pow(10, d - 1)) % 10;
	}
}

最后,还有一点点小知识~

        追求性能优——》快排

        追求省空间——》堆排

        追求稳定性——》归并

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值