常见排序算法及JAVA实现

本文介绍了各种排序算法,包括简单选择排序、堆排序、插入排序、希尔排序、冒泡排序、鸡尾酒排序、快速排序、归并排序、计数排序、基数排序和桶排序。文章详细讲解了每种排序算法的思想、优化方法和JAVA实现,通过动态图和实例展示了排序过程。文章最后对排序算法进行了性能比较,并提供了代码下载链接。
摘要由CSDN通过智能技术生成

排序算法的分类

先看维基百科中的一张关于排序算法的表

排序算法的分类
我们主要了解常见的一些排序算法。像Bogo排序,臭皮匠排序这类完全不实用的排序可以置之不理。

我们这里要说的排序算法都是内排序,也就是只在内存中进行,涉及到对磁盘等外部存储设备中的数据进行排序称之为外排序,关于外排序的内容可以查看维基百科

排序算法分类

简单选择排序(SelectSort)

选择排序思想很简单,对所有元素进行遍历,选出最小(或最大)的元素与第一个元素进行交换,然后逐次缩小遍历的范围。
选择排序动态gif图

关于八大排序算法的动态图,这里有一个网站我觉得特别好。

Java实现

import org.junit.Test;

public class SelectSort implements SortAlgorithm {
   
	public <TYPE extends Comparable<? super TYPE>> void sort(TYPE[] items) {
   
		for (int i = 0; i < items.length; i++) {
   
			int minIndex = i;
			for (int j = i; j < items.length; j++) {
   
				if (items[j].compareTo(items[minIndex]) < 0) {
   
					// items[j] < items[maxIndex]
					minIndex = j;
				}
			}
			TYPE temp = items[i];
			items[i] = items[minIndex];
			items[minIndex] = temp;
		}
	}

	// 测试代码
	private final Integer[] testItems = {
    3, 6, 2, 5, 9, 0, 1, 7, 4, 8 };

	@Test
	public void testSelectSort() {
   
		System.out.print("排序前:");
		System.out.println(Arrays.toString(testItems));
		sort(testItems);

		System.out.print("\n排序后:");
		System.out.println(Arrays.toString(testItems));
	}
}

运行结果

排序前:[3, 6, 2, 5, 9, 0, 1, 7, 4, 8]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

进阶版选择排序----堆排序(HeapSort)

堆排序就是对上面的选择排序进行了优化。从上面的简单选择排序实现中,我们可以看出,内部的循环负责比较选出最小值,然后进行一次交换。也就是说循环n次最后只选了个最小值和最前面的进行交换,前面的循环比较得出来的结果就这样抛弃了。举个很简单的例子“8, 9, 4, 6”这个无序的数组,第一次循环中已经得到8 < 98 > 6这个结论,第二次循环又将9和6进行比较,很明显第一次循环已经可以得出了9>6这个结论了,9和6比较完全是浪费的,但是简单选择就是这么傻逼无脑,那有没有一种方法让前面循环进行比较得到的结果能保存起来。这就涉及到一个概念了-----二叉堆。
二叉堆听起来很牛逼的样子,其实说白了就是一个完全二叉树。完全二叉树是什么?就是去掉满二叉树后面若干个叶子结点,看下图:
满二叉树   完全二叉树
树有很多特性,我们可以利用其层次性,让较小的元素下沉,较大的元素往上排。
二叉堆就是满足对于任意的子树都有父节点大于其子孙结点,于是最大的元素就到树顶的根节点了,这种堆就叫做大顶堆;相反小元素往上排,那就叫小顶堆了。因为二叉堆是完全二叉树,而且我们已经知道二叉堆最大的元素个数就是我们要排序的元素个数,我们的二叉堆可以就地取材直接用数组实现,所以堆排序不需要重新分配内存,空间复杂度为O(1),记得大一看“堆排序”这个名字老以为要多分配一份内存。
二叉堆的实现

看上图的时候需要注意,这里的索引是从0开始的,和我们常用的从1开始编号得到的结论可能不同:

  • 索引值为n的左结点索引值为2*n+1,右结点索引值为2*n+2;
  • 索引值为n的父节点索引值为(n-1)/2,这里的“/”是整型变量相除得到的结果也是整型变量(学计算机的都懂_)。
  • 结点数为size的完全二叉树非叶子结点数目为size/2,所以最后的那个非叶子结点的索引应该为size/2-1(因为从0开始,所以要减一)
  • 结点数为size的完全二叉树叶子结点数目为size/2size/2+1,这个结论在具体实现的时候用不到

下面这张Gif图完美地呈现了堆排序的过程:用大顶堆选出最大的,然后与最后项交换。
堆排序
看图容易实现难呐,难点主要在两个地方:

  1. 怎么将无序的数组构造成二叉堆。
  2. 将堆顶最大值与最后项交换后,如何再次调整二叉堆。

JAVA实现

import org.junit.Test;

public class HeapSort implements SortAlgorithm {
   

	public <TYPE extends Comparable<? super TYPE>> void sort(TYPE[] items) {
   
		// 构造二叉堆
		// 从最末端也就是最下面的非叶子结点开始进行调整
		// 从而使得最大值上移到二叉堆顶端
		for (int i = items.length / 2 - 1; i >= 0; i--) {
   
			adjust(items, i, items.length);
		}

		for (int i = items.length - 1; i > 0; i--) {
   
			// 将最大值与最后项进行交换
			TYPE temp = items[0];
			items[0] = items[i];
			items[i] = temp;

			// 取出最大值之后,重新调整二叉堆
			// 二叉堆也随着变量i慢慢缩小
			adjust(items, 0, i);
		}
	}

	private <TYPE extends Comparable<? super TYPE>> void adjust(TYPE[] items, int index, int heapSize) {
   
		int leftChild = 2 * index + 1;
		int rightChild = 2 * index + 2;

		// 在三个结点中选出最大的结点
		int indexOfMax = index;

		if (leftChild < heapSize) {
   // 左子树存在性检验
			if (items[leftChild].compareTo(items[indexOfMax]) > 0) {
   
				indexOfMax = leftChild;
			}
		}
		if (rightChild < heapSize) {
   // 右子树存在性检验
			if (items[rightChild].compareTo(items[indexOfMax]) > 0) {
   
				indexOfMax = rightChild;
			}
		}

		if (indexOfMax != index) {
   
			// 将较大值上移
			TYPE temp = items[index];
			items[index] = items[indexOfMax];
			items[indexOfMax] = temp;

			// 千万别漏了这个等号,我调试了半天才发现这个错误
			if (indexOfMax <= heapSize / 2 - 1) {
   
				// 如果被调整后的结点也是非叶子结点
				// 需要对该子树进行调整
				adjust(items, indexOfMax, heapSize);
			}
		}
	}

	// 测试代码
	private final Integer[] testItems = {
    3, 6, 2, 5, 9, 0, 1, 7, 4, 8 };

	@Test
	public void testHeapSort() {
   
		System.out.print("排序前:");
		System.out.println(Arrays.toString(testItems));
		sort(testItems);

		System.out.print("\n排序后:");
		System.out.println(Arrays.toString(testItems));
	}
}

有一个“平滑排序”算法和堆排序有点类似,“平滑排序”使用的是另一种二叉树----Leonardo树。关于“平滑排序”的各种说明,这里有篇文章可供参考。

简单插入排序(InsertSort)

插入排序原理也很简单,和整理扑克牌有点像。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbwmXhjZ-1581237081330)(http://docs.huihoo.com/c/linux-c-programming/images/sortsearch.sortcards.png)]
在未排序的部分选择一个元素,插入到已排序的部分,插入时会将比这个值大的所有元素往后挤。下面有动态图,看的很清楚。
插入排序

JAVA实现

import org.junit.Test;

public class InsertSort implements SortAlgorithm {
   
	public <TYPE extends Comparable<? super TYPE>> void sort(TYPE[] items) {
   
		for (int i = 1; i < items.length; i++) {
   
			TYPE current = items[i];
			int j = i - 1;
			do {
   
				if (current.compareTo(items[j]) < 0) {
   
					items[j + 1] = items[j];// 后移
				} else {
   
					break;
				}
				j--;
			} while (j >= 0);
			items[j + 1] = current; // 插入
		}
	}

	// 测试代码
	private final Integer[] testItems = {
    3, 6, 2, 5, 9, 0, 1, 7, 4, 8 };

	@Test
	public void testInsertSort() {
   
		System.out.print("排序前:");
		System.out.println(Arrays.toString(testItems));
		sort(testItems);

		System.out.print("\n排序后:");
		System.out.println(Arrays.toString(testItems));
	}
}

进阶版插入排序----希尔排序(ShellSort)

希尔排序也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序这名字可能不好理解,因为这是设计者的名字,但是提到“缩小增量排序”这个名字,可能你就已经理解一小半了。
既然说插入排序不是最高效的,那我们来想想怎么能将其进行优化吧。从上面的代码可以看出内层的循环是负责为current找到插入的位置,这是在有序的数组中查找位置,我第一个想到的就是二分查找(可能是对二分查找太敏感),但是细想之后,你会发现即使你找到那个插入点,但是你还是得将插入点后面的元素往后移动腾出个空位,这始终避免不了上面实现代码的内层循环操作。
维基百科中是这样概括插入排序的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

于是呢,“希尔”大神发明了一种算法,让插入排序移动的步伐变大,元素可以一次性朝最终目标前进一大步,从而避免了大量的数据移动。希尔排序图片不好找,下面的图片凑合着看吧:
希尔排序图解

步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
Donald Shell最初建议步长选择为 n/2 ,并且对步长取半直到步长达到1。虽然这样取可以比 O(n*n) 类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。----摘自维基百科

从维基百科的解释可以看出步长序列的选择是希尔排序的关键,一般来说我们选择的初始步长都为n/2,然后依次取半,直至为1,而且最终步长为1后才能终止,否则序列中某些元素仍是乱序。下面的JAVA实现中的步长就是依据以此。

维基百科中也提到了一些特殊步长序列,这些步长序列经过精心设计,而且使用这些精心设计的步长序列,排序速度会得到提升,详细可参考维基百科

JAVA实现

import org.junit.Test;

public class ShellSort implements SortAlgorithm {
   
	public <TYPE extends Comparable<? super TYPE>> void sort(TYPE[] items) {
   
		for (int step = items.length / 2; step > 0; step /= 2) {
   
			// 内层其实就是一个步长为step的插入排序
			for (int i = step; i < items.length; i++) {
   
				TYPE current = items[i];
				int j = i - step;
				do {
   
					if (current.compareTo(items[j]) < 0) 
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值