[排序算法] 各类经典排序算法(动态图,Java实现)

写在前面

写作本文的目的,是想对各种经典排序算法做一个自己的归纳总结,与大家一起分享。

先来看一张图,对各类排序算法有个大致的了解。图是从网上借鉴的:
各类排序算法比较
稳定性是指这么一种情况:

对于元素a和b,排序前:a等于b,并且a在b的前面。排序后:
如果a还在b的前面,该算法就是稳定的;
如果b有可能在a的前面,该算法就是不稳定的。

我个人认为算法稳定性不具有普遍意义,与算法的具体实现有密切的关系,不好统一归纳。

下面来看各算法的具体分析吧。

基于比较的排序算法

给定一个乱序元素集合,通过元素间的各种比较策略,最终得到有序的元素序列,这样的算法就称之为“基于比较的排序算法”。

选择排序

这是最简单的排序算法,也是最好理解的算法,就是一遍遍的从乱序元素集合中挑出最值元素,依次排好即可。

算法实际效率比较低,一般不常用,时间复杂度是O(N2)。

算法描述
  1. 遍历N个元素集合,找出最值元素,与第一个元素交换位置
  2. 遍历剩下N-1个元素,找出最值元素,与数组第二个元素交换位置
  3. 重复上述过程N-1次,完成排序
  4. 优化点:如果某次遍历过后,发现最值元素的位置就是交换位置,则无需交换
动图演示

选择排序 动图演示

代码实现
public static void sort(int[] arr) {
	int len = arr.length;
	for (int i = 0; i < len - 1; i++) {
		int minIndex = i;
		for (int j = i + 1; j < len; j++) {
			if (arr[j] < arr[minIndex]) minIndex = j;
		}
		if (minIndex == i) continue;
		int temp = arr[i];
		arr[i] = arr[minIndex];
		arr[minIndex] = temp;
	}
}

冒泡排序

这种排序算法简单,也不难理解,可以联想体育老师根据学生身高调整队列的场景。

算法实际效率比较低,不常用,时间复杂度是O(N2)。

算法描述
  1. 从左到右,依次比较相邻两元素,如果左元素比右元素大,就交换位置,然后继续向后比较。一轮比较下来,最大的元素就会被移动到最右端
  2. 最大元素位置确定后,在前N-1个元素中重复上述步骤
  3. 经过N-1轮比较调整后,完成排序
动图演示

冒泡排序 动图演示

代码实现
public static void sort(int[] arr) {
	int len = arr.length;
	for (int i = 0; i < len - 1; i++) {
		for (int j = 0; j < len - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

插入排序

也是很简单很好理解的排序算法,可以联想玩扑克牌的场景。对于新来的元素,要与已经排好序的元素队列挨个进行比较,找到自己的位置后插入队列。

最好时间复杂度O(N),最坏时间复杂度O(N2)。

算法描述
  1. 假设第一个元素是已经排好序的元素
  2. 取出下一个元素,空出一个位置,向前依次与有序元素进行比较,如果有序元素大于当前元素,则有序元素后移一位,直到有序元素不大于当前元素时,将当前元素插入空位
  3. 重复上述过程,完成排序
动图演示

插入排序 动图演示

代码实现
public static void sort(int[] arr) {
	int len = arr.length;
	for (int i = 1; i < len; i++) {
		int temp = arr[i];
		int j = i - 1;
		for (; j >= 0 && arr[j] > temp; j--) {
			arr[j + 1] = arr[j];
		}
		arr[j + 1] = temp;
	}
}

希尔排序

希尔排序是对插入排序的一种改进优化,是第一个突破O(n2)的排序算法。

希尔排序的解题思想是把序列按一定的间隔分组,对每组使用插入排序,然后不断的减小间隔,直到间隔值等于0的时候,整个序列就是有序的。

语言说不太清楚,看图更直观,结合动态图去理解吧。

算法描述
  1. 初始化gap间隔值,通常取集合长度length的一半,也就是gap=length/2,gap值的含义既是分组数量,也是每组元素的间隔长度
  2. 对所有分组进行插入排序
  3. gap=gap/2,得到新的分组,再对新分组进行插入排序,重复该过程,直到gap等于0
  4. gap等于0时,排序也就完成了
动图演示

希尔排序 动图演示

代码实现
public static void sort(int[] arr) {
	for (int gap = arr.length / 2; gap > 0; gap /= 2) { // gap 步长
		// 代码实现是多个分组交替执行,动态图是按组依次执行,这就是 i++ 的原因
		for (int i = gap; i < arr.length; i++) {
			if (arr[i] >= arr[i - gap]) continue;// 本次没有调整的必要
			// 当前元素排序不正确,对该分组进行插入排序
			int temp = arr[i];
			int j = i - gap;
			while (j >= 0 && arr[j] > temp) {
				arr[j + gap] = arr[j];
				j -= gap;
			}
			arr[j + gap] = temp;
		}
	}
}

归并排序 (非递归实现)

归并排序的思路是分治思想,将大问题分解了多个相似的小问题,通过求解多个小问题,最终合并出大问题的解。

归并排序的时间复杂度是O(nlogn)。

算法描述
  1. 将N个元素分散为N个区间,每个区间只有1个元素,所以每个区间的元素是有序的,这里称之为N个“1元素区间”
  2. 对两个相邻的“1元素区间”进行合并排序,得到N/2个有序的“2元素区间”
  3. 对两个相邻的“2元素区间”进行合并排序,得到N/4个有序的“4元素区间”
  4. 继续合并相邻区间,直到N个元素处于同一区间,完成排序
动图演示

归并排序 动图演示

代码实现

网上很多人的代码都是采用递归实现,数据量大的时候,递归就会有栈溢出的问题。

我这里不写递归了,采用非递归思路来实现归并排序算法:

public static void sort(int[] arr) {
	int len = arr.length, space = 1;
	while (space < len) {
		space *= 2; // 2,4,8,16 ... 每次归并区间的数组长度
		for (int low = 0; low < len; low += space) {
			int high = low + space - 1, mid = (low + high) / 2;
			if (mid + 1 >= len) continue;
			merge(arr, low, mid, high < len ? high : len - 1);
		}
	}
}

public static void merge(int arr[], int low, int mid, int high) {
	// 数组 arr[low..mid], arr[mid+1..high] 都是排好序的
	int len = high - low + 1;
	int[] temp = new int[len];// 用了一个额外数组空间,可优化掉
	int left = low, right = mid + 1, index = 0;
	while (left <= mid && right <= high) // 归并
		temp[index++] = (arr[left] <= arr[right]) ? arr[left++] : arr[right++];
	while (left <= mid) temp[index++] = arr[left++];
	while (right <= high) temp[index++] = arr[right++];
	for (int k = 0; k < len; k++) arr[low + k] = temp[k];
}

快速排序 (递归实现)

快速排序算法也是分治思想的一种实践,将大问题化解为小问题求解。它的思路是将乱序元素集合一分为二,两个子集合整体有序,再将子集合一份为二,保证更小的子集合也是整体有序的,就这么一直分下去,直到所有的子集合的元素只有一个的时候,整体就有序了。一分为二的代码实现就可以借助递归思路了。

快速排序、归并排序,这两种算法都是分治思想典型的实践。但是它们的实现思路却是截然相反的:快速排序是先对整体排序再对局部排序,从大到小的解决问题,归并排序是先对局部排序再对整体排序,从小到大的解决问题。

快速排序是个很常用的算法,在实际应用中,为了达到更好的效果,在各种细节上的会有进一步优化。

这里只讲快速排序的算法理论模型。

算法描述
  1. 挑选一个元素,作为划分两个子区间的基准元素,通常取第一个元素
  2. 将基准元素作为左区间唯一的元素,也当作是左区间最大的元素,剩下的元素先暂时归入右区间
  3. 遍历右区间元素,与基准元素比较,将小于基准值的元素交换到右区间的左侧,同时在概念上将它们归入左区间
  4. 右区间遍历完成以后,将基准元素与左区间最右端元素交换位置,也就是左区间两端元素互换
  5. 此时,基准元素就位于左区间最右侧,所以,基准元素左侧都是比它小的元素,右侧都是比它大的元素
  6. 以基准元素作为分界线,对其左右两个区间的元素分别重复上述过程,直到每个区间只含有一个元素的时候,排序完成
动图演示

快速排序 动图演示

代码实现
public static void sort(int[] arr) {
	quickSort(arr, 0, arr.length - 1);
}

public static void quickSort(int[] arr, int left, int right) {
	if (left >= right) {
		return;
	}
	int key = arr[left], empty = left;
	int[] index = new int[]{left, right};
	while (left < right) {
		if (empty == left) {
			if (arr[right] < key) {
				arr[left++] = arr[right];
				empty = right;
			} else right--;
		} else {
			if (arr[left] > key) {
				arr[right--] = arr[left];
				empty = left;
			} else left++;
		}
	}
	arr[empty] = key;
	quickSort(arr, index[0], empty - 1);
	quickSort(arr, empty + 1, index[1]);
}

不基于比较的排序算法

从这里开始,接下来的排序思想就不再是基于元素的比较了,而是借助于数学上的关系映射思想。

实现这些算法前,需要找到某种映射关系,可以将乱序元素映射到另一种状态,并且,另一种状态当中隐藏着有序的关系,借助这种有序的关系,可以反推出乱序元素的排序。

计数排序

计数排序算法,是将乱序元素映射成键值关系,键是元素本身,值是元素出现的次数。把键当成数组下标,它就是有序的,也就是说,乱序元素可以存储在下标有序的数组里面,通过数组下标的有序性可以推导出乱序元素的有序性。

当乱序元素取值范围比较集中,且左右差值不大的时候,该算法才会有比较良好的表现。

算法描述
  1. 遍历乱序元素,找到最小值元素和最大值元素,确定乱序元素取值区间
  2. 根据取值区间,构建出合适的数组存储结构
  3. 将乱序元素映射成数组下标,存入数组空间
  4. 根据排序规则,顺序访问数组并取出元素,完成排序
动图演示

计数排序 动图演示

代码实现
public static void sort(int[] arr) {
	int len = arr.length, min = arr[0], max = arr[0];
	// 确定取值范围
	for (int i = 1; i < len; i++) {
		if (arr[i] < min) min = arr[i];
		if (arr[i] > max) max = arr[i];
	}
	// 用数组构建哈希存储结构,数组下标 index + min 就是取值范围,数组值就是排序元素出现的次数
	int[] temp = new int[max - min + 1];
	for (int i = 0; i < len; i++) temp[arr[i] - min]++;
	int index = 0;
	for (int i = 0; i < temp.length; i++) {
		while (temp[i]-- > 0) arr[index++] = min + i;
	}
}

桶排序

很形象的算法名称,就是准备多个装元素的桶,并且,桶要有顺序性,然后根据某种映射关系,将乱序元素装入桶中,此时,每个桶中的元素是乱序的,但是桶区间整体是有序的,最后,对每个桶中的元素再进行排序,从而达到整体的有序性。

对单个桶的元素再排序,可以选择继续用桶排序算法,但是通常会选择其它更简单的算法排序,因为映射关系通常是不好找的,并且也没有一种放眼四海而皆准的映射关系。

仔细想想,其实桶排序和快速排序很像,都是将大区间分为小区间,在小区间里对元素进行排序,都有点分治思想。但是它们有本质区别:快速排序是一种原地排序算法,乱序元素之间存在相互作用关系(元素比较),桶排序不是原地排序算法,乱序元素在映射的过程中也不存在相互作用关系。

好的映射关系能够有效提高算法效率。

算法描述

桶是一种解决问题的思路,桶排序算法没有一个固定的编程范式,为了理解,这里假设一种排序场景:对3位数以内的非负整数集合进行排序,映射关系是相同位数的数字映射到一个桶内。

  1. 准备三个桶,分别接收1位数字、2位数字、3位数字
  2. 根据映射关系,将集合元素映射到三个桶中
  3. 对每个桶内的元素子集合进行排序
  4. 按顺序从桶中取出元素,完成排序
动图演示

桶排序 动图演示

代码实现

代码不具备通用性,只针对上述假设的排序场景:

public static void sort(int[] arr) {
	// 1位数桶,2位数桶,3位数桶
	List<Integer>[] buckets = new ArrayList[3];
	// 初始化桶
	for (int i = 0; i < buckets.length; i++) buckets[i] = new ArrayList<>();
	// 元素根据映射关系入桶
	for (int i = 0; i < arr.length; i++) {
		int temp = arr[i];
		if (temp / 10 == 0) buckets[0].add(temp);
		else if (temp / 100 == 0) buckets[1].add(temp);
		else buckets[2].add(temp);
	}
	// 桶排序
	for (int i = 0; i < buckets.length; i++) Collections.sort(buckets[i]);
	// 元素归位
	int index = 0;
	for (int i = 0; i < buckets.length; i++) {
		List<Integer> bucket = buckets[i];
		for (Integer integer : bucket) arr[index++] = integer;
	}
}

基数排序

基数排序算法依赖于元素自身的特性,适用于非负整数这一类的元素排序,用非负整数来理解该算法也更容易一些。

每个整数都可以拆分为个位、十位、百位等等,对非负整数排序,可以先根据个位数字排序一次,再根据十位数字排序一次,再根据百位数字排序一次,以此类推,从低位到高位都排经过一次排序后,整体元素就是有序的。

算法描述
  1. 找出所有元素中最大的数,也就是位数最多的元素,有几位就需要进行几次基数排序
  2. 从最低位(个位)开始进行基数排序,这个过程中,所有元素会进入基数桶,然后按排序规则从桶中取出元素顺序排列
  3. 从低位到高位,重复第二个过程,完成排序
动图演示

基数排序 动图演示

代码实现
public static void sort(int[] arr) {
	// 找出最大数
	int max = -1;
	for (int num : arr) if (num > max) max = num;

	int count = String.valueOf(max).length();// 需要进行几次基数排序
	int auxiliary = 1;// 辅助数,可用它求解每一位的数字
	List<Integer>[] buckets = new ArrayList[10];// 0-10的基数桶
	for (int i = 0; i < buckets.length; i++) buckets[i] = new ArrayList<>();// 初始化桶元素
	while (count-- > 0) {
		// 这个for循环是让排序元素进入基数桶
		auxiliary *= 10;
		for (int i = 0; i < arr.length; i++) {
			int temp = arr[i];
			int num = (temp % auxiliary) / (auxiliary / 10);// 个位、十位、百位...数字
			buckets[num].add(temp);
		}
		// 这个for循环是按排序规则,把基数桶中的元素放回原始数组
		int index = 0;
		for (int i = 0; i < buckets.length; i++) {
			List<Integer> bucket = buckets[i];
			for (Integer integer : bucket) arr[index++] = integer;
			bucket.clear();
		}
	}
}

总结

常用经典排序算法应该总结的差不多了,还有一个堆排序没写,因为没有找到非常合适的动态图,以后有空再补上。

代码实现都是自己写的,细节之处还有改进的地方。

感谢VisuAlgo网站提供的部分动态图,希尔排序和桶排序的动态图是我自己画的。

文章较长,写作不易,看完点个赞呗。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值