算法面试之排序算法(四)—— 选择排序、冒泡排序、插入排序、归并排序、随机快排

4 篇文章 0 订阅
4 篇文章 0 订阅

排序算法

排序算法关键指标

排序算法三个重要指标:时间复杂度、空间复杂度、稳定性

排序算法的稳定性

  • 稳定性是指同样大小的样本再排序之后不会改变相对次序
  • 对基础类型来说,稳定性毫无意义
  • 对非基础类型来说,稳定性有重要意义
  • 有些排序算法可以实现成稳定的,而有些排序算法无论如何都实现不成稳定的

给各位看官解释下
稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面。
不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面。

常见排序算法优劣

在这里插入图片描述

排序算法总结

1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是O(NlogN)
4)时间复杂度O(N
logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
5)为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并

排序递归的时间复杂度

在以下排序过程中,多次使用递归,形似以下形式的递归,满足master公式

	int mid = L + ((R - L) >> 1);
	int leftMax = process(arr, L, mid);
	int rightMax = process(arr, mid + 1, R);

master公式:
T(N) = a * T(N/b) + O(N^d)(其中的a、b、d都是常数)
的递归函数,可以直接通过Master公式来确定时间复杂度
如果 log(b,a) < d,复杂度为O(N^d)
如果 log(b,a) > d,复杂度为O(N^log(b,a))
如果 log(b,a) == d,复杂度为O(N^d * logN)
其中a是递归调用次数,b为遍历数据规模的1/b,N^d为非递归其他步骤的时间复杂度

如上:
a=2
b=2(规模为之前的1/2,所以b=2)
d=0(除递归外,其他时间复杂度为O(1) = O(N^d),N ^d=1 ,d=0)
时间复杂度 T(N) = O(2*N/2+1)=O(n)

选择排序

选择排序(Select Sort) 是直观的排序,通过确定一个 Key 最大或最小值,再从带排序的的数中找出最大或最小的交换到对应位置。再选择次之。双重循环时间复杂度为 O(n^2)

算法描述

  1. 在一个长度为 N 的无序数组中,第一次遍历 n-1 个数找到最小的和第一个数交换。
  2. 第二次从下一个数开始遍历 n-2 个数,找到最小的数和第二个数交换。
  3. 重复以上操作直到第 n-1 次遍历最小的数和第 n-1 个数交换,排序完成。

过程演示

动图制作网站:VisAlgo
在这里插入图片描述

代码实现

public static void selectionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// 0~n-1
		// 1~n-1
		// 2~n-1
		for (int i = 0; i < arr.length - 1; i++) { // i ~ N-1
			// 最小值在哪个位置上  i~n-1
			int minIndex = i;
			for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下标 
				minIndex = arr[j] < arr[minIndex] ? j : minIndex;
			}
			swap(arr, i, minIndex);
		}
	}

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

冒泡排序

冒泡排序(Bubble Sort) 最为简单的一种排序,通过重复走完数组的所有元素,通过打擂台的方式两个两个比较,直到没有数可以交换的时候结束这个数,再到下个数,直到整个数组排好顺序。因一个个浮出所以叫冒泡排序。双重循环时间 O(n^2)

算法描述

  1. 比较相邻两个数据如果。第一个比第二个大,就交换两个数
  2. 对每一个相邻的数做同样1的工作,这样从开始一队到结尾一队在最后的数就是最大的数。
  3. 针对所有元素上面的操作,除了最后一个。
  4. 重复1~3步骤,知道顺序完成。

过程演示

动图制作网站:VisAlgo
在这里插入图片描述

代码实现

public static void bubbleSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e
			for (int i = 0; i < e; i++) {
				if (arr[i] > arr[i + 1]) {
					swap(arr, i, i + 1);
				}
			}
		}
	}

	// 交换arr的i和j位置上的值
	public 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];
	}

上面交换的步骤,采用亦或运算,在算法面试之基本概念(一)中有具体讲解。

插入排序

每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。

算法描述

  1. 从左往右,从第二个参数开始,依次向后的参数和前面的参数对比
  2. 假如按照从小到大排列,将参数插入到已经排好序的位置当中
  3. 递归一次,比较一次,时间复杂度O(n^2)

过程演示

动图制作网站:VisAlgo
在这里插入图片描述

代码实现

	public static void insertionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// 0~0 有序的
		// 0~i 想有序
		for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
			
			// arr[i]往前看,一直交换到合适的位置停止
			// ...(<=)  ?       <- i
			for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
				swap(arr, j, j + 1);
			}
		}
	}

	// i和j是一个位置的话,会出错
	public 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];
	}

归并排序

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

算法图解

在这里插入图片描述
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为logn
我们再具体看下治的过程(合并过程)
在这里插入图片描述

算法演示

动图制作网站:VisAlgo
在这里插入图片描述

代码演示

	// 递归方法实现
	public static void mergeSort1(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		process(arr, 0, arr.length - 1);
	}

	public 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);
	}
	public static void merge(int[] arr, int L, int M, int R) {
		//一个空数组排序使用,以下称为排序数组
		int[] help = new int[R - L + 1];
		int i = 0;
		int p1 = L; //分区,上区排序指针
		int p2 = M + 1; //分区,下区排序指针
		while (p1 <= M && p2 <= R) { //两个指针都不越界时
			//对比,将指针小值复制到排序数组,排序数组和取值数据指针移动
			help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
		}
		//注意以下两个while只会执行一次,因为每次p1和p2只移动一个
		//将未越界指针指向的分区数组全部复制给排序数组
		while (p1 <= M) {
			help[i++] = arr[p1++];
		}
		while (p2 <= R) {
			help[i++] = arr[p2++];
		}
		//最后总体遍历一遍,如果有数组没有赋值,补全数组,排序完成
		for (i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
	}

随机快排

快速排序(QuickSort)是排除稳定性因素后最常用的排序,下面用递归方法实现。

算法描述

  1. 从数列中挑出一个元素作为基准。
  2. 重新排列数列,把所有的比基准小的放在基准前面,反之放在后面(一样大可任意一边)完成后基准处在分区的中间位置。
  3. 通过递归调用把小于基准元素和大雨基准元素的子序列进行排序。

算法图示

动图制作网站:VisAlgo
在这里插入图片描述

代码实现


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

	public static void process(int[] arr, int L, int R) {
		if (L >= R) {
			return;
		}
		//随机快排
		int M = partition(arr, L, R);
		//分区递归
		process(arr, L, M - 1);
		process(arr, M + 1, R);
	}
	
	public static int partition(int[] arr, int L, int R) {
		if (L > R) {
			return -1;
		}
		if (L == R) {
			return L;
		}
		int lessEqual = L - 1;
		int index = L;
		while (index < R) {
			if (arr[index] <= arr[R]) {
				swap(arr, index, ++lessEqual);
			}
			index++;
		}
		swap(arr, ++lessEqual, R);
		return lessEqual;
	}
	//交换ij位置
	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

更多精彩关注微信公众号

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值