算法(二)简单排序

友情链接

算法(一)基础数据结构

算法(三)复杂排序

前言

在讲解数据结构高级用法前,我左思右想还是觉得排序应该首先回顾一下排序的相关算法。因为如优先队列、字符串排序、链表排序等都需要排序的知识,而排序也是算法中最基础的部分,这一篇就先介绍一下基础的几种排序方式,后续会继续更新。
主要介绍: 冒泡排序、插入排序、选择排序、希尔排序、快速排序。
在准备时也看到了十大排序算法这篇文章,很有参考意义。当然同一种算法的思想都是一致的,但是编程的思想却并不相同。

算法时间复杂度

算法时间复杂度空间复杂度是否稳定
冒泡排序N21
选择排序N21
插入排序N~N21
希尔排序N6/5~NlogNlogN
快速排序NlogN~N2logN

正文

本节讲解的基础排序算法都是交换排序算法,因为这五种排序算法都是通过交换元素来实现元素有序,其时间复杂度也主要考察交换的次数。

基础

这里的方法使用了Comparable接口定义的数组,因为大多数排序使用的场景不一定只是基础数据结构,而是两个对象间进行比较,只有类实现了Comparable接口中的CompareTo方法后,两个对象才能进行比较。而基础数据结构的封装类(如Integer、Float、Long等)都已经实现了这种方法,因此对于基础数据结构也可以使用;当然如果只是对基础数据结构排序,如同上篇博客一样,使用泛型的方式也是非常合理的。

public void exchange(Comparable[] a, int i, int j){
	Comparable temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}
public boolean isLess(Comparable[] a, int i, int j){
	return a[i].compareTo(a[j])<=0?true:false;
}

冒泡排序

基本分析

冒泡排序 是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

实现
public void bubbleSort(Comparable[] a){
	for(int i=0;i<a.length;i++){
		for(int j=a.length-1;j>i;j++){
			if(a[j-1]>a[j])
				exchange(a, j-1, j);
		}
	}
}

最基础但是也最容易犯错,一定注意数组的边界问题以及冒泡方向。

选择排序

基本分析

选择排序(Selection-sort) 是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
由于属于简单算法,就一笔带过。

实现

public void ElectionSort(Comparable[] a){
	for(int i=0;i<a.length;i++){
		int min = i;
		for(int j=i+1;j<a.length;j++){
			if(a[min]>a[j]) min=j;
		}
		exchange(a, min, i);
	}
}

插入排序

基本分析

关于插入排序的来源还是挺有意思的,是由打扑克时在摸牌后将当前牌向手牌中插入到正确位置,到不断循环此过程,最后你的手牌就是有序的了。(果然算法来源于生活)
插入排序(Insertion-Sort) 的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

实现

这里实现插入排序有两种思想:
(1)将要插入的元素不断与比它大的元素交换,直至出现一个比它小的元素或者该元素已经换到最前面
(2)找到比它小的第一个元素位置,将后面的比插入元素大的元素集体向后挪动一个位置,然后将插入元素放到空出的位置。

实现方式1
public void insertionSort(Comparable[] a){
	for(int i=0;i<a.length;i++){
		int j=i+1;
		while(j>0&&isLess(a, j, j-1)){
			exchange(a, j, j-1);
			j--;
		}
	}
}
实现方式2
public void insertionSort(Comparable[] a){
	for(int i=0;i<a.length;i++){
		int j = i+1;
		Comparable target = a[j];
		while(j>0&&isLess(a, j, j-1)){
			a[j]=a[j-1];
		}
		a[j] = target;	
	}
}

实现方式2要比实现方式1稍微高效一点,因为交换两个元素需要访问3次数组,而挪动只需要访问2次,并且实现方式2每次外循环只需要创建一个临时变量,而实现方式1每次交换就需要创建一次。当然这并不太影响时间复杂度的级别,两者相差甚微。
从插入排序的实现来看,如果数组基本有序,那么插入排序几乎不需要对插入元素进行移动,其时间复杂度将会变为惊人的O(n)!这当然是一个非常有意义的事情,因为优化基本就是从这里展开的。

希尔排序

基本分析

希尔排序 开始,人们开始慢慢意识到了 分治法 的重要性。因为插入排序对于基本有序的数组将会非常快速,因此希尔排序诞生了!通过对子集进行排序,让整个数组基本有序,最终的插入排序只需要挪动很少的元素就可以完成排序。
而希尔排序的实现是对相隔指定步长的元素集合进行排序,不断减小步长,直到步长减为1,退化成 插入排序 进行一遍完整排序,从而完成排序。因此 希尔排序始终不可能到达插入排序O(n)的最好情况 ,因为其对步长大于1的元素集合还存在着比较操作。
因此可以看出,在数组基本有序的情况下,使用插入排序的效率可能会比希尔排序还要高,当然快排更不用说了,因此对于不同的数组状况挑选不同的算法还是很必要的,因为没有哪种算法对所有数组总是时间复杂度最低。

算法实现

希尔排序最重要的是确定步长数组,通常情况下选取xt=3*xt-1+1的数组作为希尔排序的步长。

public void shellSort(Comparable[] a){
	int N = a.length;
	int h =1;
	while(h<N/3){
		h = 3*h+1;
	}
	while(h>=1){
		//使得数组变为h有序
		for(int i=h;i<N;i++){
			for(int j=i;j>=h&&isLess(a, j, j-h);j-=h)
				exchange(a, j, j-h);
		}
		h/=3;
	}
}

很明显希尔排序是不稳定的,因为其步长有序的情况下非常有可能将相等元素的次序颠倒,如 {5,3(1),3(2),4}step=2 时,首先对 {5, 3(2)} 排序,将会使得集合变为 {3(2),3(1),5,4} ,显然此时两个3的顺序已经颠倒。

快速排序

基本分析

自从发现了时间复杂度首次降为O(n2)以下的希尔排序后,人们开始发现分治法的用处非常大,并且发现分别处理两个小的集合要比处理大的集合交换次数会少得多。
{4,1,2,3,5,6,2} 这样的集合,从前往后对每个元素进行插入,当到达最后一个元素2时,集合变为 {1,2,3,4,5,6,2} ,此时最后一个2需要交换4次才能到正确的位置,并且如果集合越长,交换的次数将会非常大。但是如果使用快速排序的算法,将4作为哨兵,把集合分成了 {2,1,2,3}{5,6} ,期间只是交换了一次,但是两个集合已经基本有序,并且2只需要再交换一次就可以落到正确的位置。
显然通过上面的例子,对这样的集合,快速排序是非常高效的,时间复杂度降为了O(NlogN)
当然快速排序致命弱点在于对基本有序的集合效率非常低,不过对于这种情况,我们也有相应的解决方案。

实现

如何打破数组的基本有序?
通常有两种方式:
(1)将原数组进行随机打乱
(2)对数组或者子数组每次随机选取哨兵
这两种方式都非常有效的帮助快速排序摆脱O(n2)时间复杂度的窘境,因此值得使用。

方式一
//将原数组进行随机打乱
public void shuffle(Comparable[] a){
	for(int i=0;i<a.length;i++){
		int random = (int)(i+ Math.random()*(a.length-i));
		exchange(a, i, random);
	}
}
//将当前分组排序,并返回哨兵最终的下标
public int partition(Comparable[] a, int low, int hi){
	int i = low;
	int j = hi;
	while(true){
		while(isLess(a, low, j--));
		while(isLess(a, ++i, low))if(i==hi)break;
		if(i>=j)break;
		//将两侧元素进行互换
		exchange(a, i, j);
	}
	//将哨兵换到正确的位置
	exchange(a, low, j);
	return j;
}
//用于递归的快速排序函数
public void QuickSort(Comparable[] a, int low, int hi){
	//注意其中low=hi时即切分长度为1,不需要再进行排序
	if(low>=hi||a.length<1||low<0||end>=a.length)
		return ;
	int privt = partition(a, low, hi);
	QuickSort(a, low, privt-1);
	QuickSort(a, privt+1, hi);
}
//调用函数
public void QuickSort(Comparable[] a){
	//先进行随机打乱
	shuffle(a);
	QuickSort(a, 0, a.length-1);
}
方式二

第二种方式相比较于第一种对partition函数有所更改,并且不再调用shuffle随机打乱函数,因此这里只展示partition函数的编写:

public int partition(Comparable[] a, int low, int hi){
	int i = low;
	int j = hi;
	int privt = (int)(low+Math.random()*(end-start+1));
	exchange(a, low, privt);
	//下面的实现与上面的完全一致
	while(true){
		while(isLess(a, low, j--));
		while(isLess(a, ++i, low))if(i==hi)break;
		if(i>=j)break;
		exchange(a, i, j);
	}
	exchange(a, low, j);
	return j;
}

这一篇就讲解这些排序算法,下一篇将讲解一些复杂排序方式,但是其时间复杂度更低,在某些场景中应用较好。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值