数据结构排序算法总结

GitHub: KolinHuang
个人博客:KolHuang Blog
欢迎交流~

写在前头:
刚刚考完研究生初试,正在紧张的准备复试当中…
怎奈实在是难以静下心好好的看书,于是想到了写博客这个办法,希望奏效……=-=

话不多说,开始吧~

提醒:本文排序均为升序,即是第一个元素为最小值。

冒泡排序

就从最为大家熟知的冒泡排序开始,
原理:
假设有一个无序数组s[n],一趟排序的过程:把第一个元素与第二个元素比较,如果第一个比第二个大,那么交换他们的位置。接着继续比较第二个元素和第三个元素的大小,如果第二个比第三个大,那么交换他们的位置,以此类推…
代码分析:
一趟排序只能将数组中的最大值挪到数组的末尾(即一次确定一个元素的最终位置),所以在每一趟排序结束后要减少待排元素个数,因此2个for循环即可搞定,废话不多说,上代码~

void Bubble_Sort(ElementType s[],int N)
{
    int i,j;
    for(i = N - 1; i >= 0; --i)
    {
        for ( j = 0; j < i; ++j)
        {
            if(s[j] > s[j+1])
                swap(s[j],s[j+1]);
        }
    }
}

但是这样的冒泡并不高效,如果有这么一种情况:当进行了k趟排序后(k<n),待排序列已经完全有序,但是程序依旧会继续进行余下比较,但都是无意义的,所以只需要设置一个flag,当某趟排序发生了交换时,则继续下一次比较,否则break。代码如下:

void Bubble_Sort(ElementType s[],int N)
{
    int i,j;
    for(i = N-1; i >= 0; --i)
    {
        int flag = 0;
        for ( j = 0; j < i; ++j)
        {
            if(s[j] > s[j+1])
            {
                swap(s[j],s[j+1]);
                flag = 1;
            }
        }
        if(flag == 0) break;
    }
}

性能分析:
仅用了常数个辅助单元,因而空间复杂度为O(1)。
平均的时间复杂度为O(n^2)。
是一个稳定的算法。(补充:稳定不稳定是看对于相等的两个元素在进行排序后,相对位置是否发生了改变,没有改变则是稳定的,否则不稳定)

快速排序

原理:
快速排序是对冒泡排序的一种改进。其基本思想是基于分治的:在待排序列s[0…n-1]中,选取一个元素pivot作为基准,通过一趟排序将原表划分为两个子表,左边的值均小于pivot,右边的值均大于pivot,而pivot也处于最终位置上。而后对两个子表进行递归排序,以此类推…
代码分析:
假设我们已经有了一个划分的辅助函数Partition(),返回值为pivot在表中的最终位置,递归地调用快速排序即可,代码如下:

void Quick_Sort(ElementType s[],int low,int high)
{
    if(low < high)//递归跳出条件
    {
        int pivotPos = Partition(s,low,high);
        Quick_Sort(s,low,pivotPos-1);
        Quick_Sort(s,pivotPos+1,high);//对两个子表递归
    }
}

然后就是重点的Partition()函数,如果我们能够选择出一个pivot值,使得其最终位置处于待排序列的中间,那么两个子表的长度相近,则递归的次数就会少很多,代码也相对高效(在这里,我选择第一个元素作为pivot,在性能分析中会提出更优的选择方案。),接下来的操作很简单,只需要将比pivot大的向右边移动,将比pivot小的向左边移动即可。代码如下:

int Partition(ElementType s[],int low,int high)
{
    ElementType pivot = s[low];
    while(low < high)
    {
        while(low < high && s[high] >= pivot) --high;
        s[low] = s[high];//将比pivot小的移到左边
        while(low < high && s[low] <= pivot) ++low;
        s[high] = s[low];//将比pivot大的移到右边
    }
    a[low] = pivot;
    return low;//此时low或者high就是pivot的最终位置
}

性能分析:
由于采用递归算法,因此需要借助一个递归工作栈来保存每一层递归调用的必要信息(如果要改成非递归算法,可以从这里出发,借用一个栈来保存信息),所以空间复杂度为O(log2n)。
时间复杂度为O(nlog2n)。
上文提到的适当pivot ,可以提高算法效率,这里给出一个方案:从序列的头尾以及中间选取三个元素,再取这三个元素的中间值作为最终的pivot即可,当然还有其他更好方案。
快速排序不是一个稳定的排序算法,值得一提的是,快速排序算法是所有内部排序算法中平均性能最优的。(当然我们要明确一个概念,这世界上没有最好的算法,每个算法都有最适合自己的应用场景,这里讨论的是平均性能~)

插入排序

插入排序有多种:直接插入排序,折半插入排序,希尔排序等。

首先来看直接插入排序

原理:将一个已知元素s[ i ]插入到已有序的子序列s[1…i-1]当中,过程分三步:1.查找出s[ i ]在子序列s[1…i-1]中的位置k。2.将k开始往后的元素后移。3插入元素s[ i ]。

代码分析:
其实我们可以将这个过程看做打牌时抽牌的过程,一般情况下,先抽牌,再落位。代码如下:

void Insertion_Sort(ElementType s[],int N)
{
    int i,j;
    for(i = 0;i < N;++i)
    {
        tmp = s[i];//抽牌
        for (j = i; j > 0&&s[j-1] > tmp ; --j)
            s[j] = s[j-1];//挪出空位
        s[j] = tmp;//落位
    }
}

折半插入排序

在直接插入排序中,我们可以将过程简化为查找到元素位置,并插入。在此基础上可以对查找过程进行优化,折半插入,顾名思义就是先通过折半查找,找到元素的位置,再插入即可。

代码如下:

void HalfInsertion_Sort(ElementType s[],int n)
{
    int i,j,low,mid,high;
    for(i = 2;i <= n; i++)
    {
        s[0] = s[i];
        low = 1;
        high = i - 1;
        while(low <= high)
        {
            mid = (low + high) / 2;
            if(a[mid] > s[0]) high = mid - 1;
            else low = mid + 1;
        } 
        for(j = i - 1;j > high + 1; --j)
            s[j+1] = s[j];
        s[high + 1] = s[0];
    }
}

希尔排序

希尔排序又成缩小增量排序,基本思想是:先将待排序列分割为形如s[i],s[i+d],s[i+2d],s[i+3d]…的特殊间隔子表,再对每个子表进行直接插入排序。最关键的环节就是如何确定增量序列,一个简单的选择就是d[1] = n/2,d[i+1] = d[i] / 2,并且最后一个增量为1。代码如下:

void Shell_Sort(ElementType s[],int N)
{
	int D,i;
	for(D = N / 2;D > 0; D /= 2)//步长变化
	{
		ElementType tmp = s[D];
		for (i = D; i >= D && s[i-D] > tmp; i -= D)
		{
			s[i] = s[i-D];
		}
		s[i] = tmp;
	}
}

下面我们对这三个常见的直接插入排序进行总结:

算法种类最好时间复杂度平均时间复杂度是否稳定
直接插入排序O(n)O(n^2)
折半插入排序O(n)O(n^2)
希尔排序O(n^1.3)O(n^d)

简单选择排序

原理:假设待排序列为s[1…n],第 i 趟排序就是从s[i…n]中选择出最小的那个与s[ i ]交换。一次确定一个最小元素的最终位置。
代码很简单,如下:

void selectSort(ElementType s[],int n)
{
    int min,i,j;
    for (i = 0; i < n - 1; ++i)
    {
        min = i;
        for ( j = i+1; j < n; ++j)
        {
            if(a[j] < s[min])   min = j;
        }
        if(min != i)    swap(s[i],s[min]);
    }

}

性能分析:
仅使用常数个辅助单元,故空间复杂度为O(n)。
时间复杂度为O(n^2),因为元素间比较的次数与序列的初始状态无关,始终是n(n-1)/2次。
并且是一个不稳定的排序算法。

堆排序

堆排序的特点是:将待排序列s[1…n]看作一颗完全二叉树,并且这颗二叉树是大顶堆(即s[ i ] > s[ 2i ] && s[ i ] > s[ 2i+1 ],父结点的键值大于左右孩子结点的键值)。每趟排序前,首先将序列调整为小顶堆,然后取出根结点,进行下一趟…

代码分析:大根堆适用于升序排序,小根堆适用于降序排序,按要求选择调整方式即可。

代码如下:

/*调整成最大堆*/
void HeapAdjust(ElementType s[],int k,int n)
{
	ElementType tmp = s[k];
	int i;
	for(i = k*2+1; i < n;i = 2*I+1)
	{
		if(i+1 < n && s[i] < s[i+1])//若右孩子大于左孩子,i+1
			i++
		if(tmp > s[i])//若s[k]已经是最大值,不做操作
			break;
		s[k] = s[i];
		k = i;
	}
	s[k] = tmp;
}

void Heap_Sort(ElementType s[],int n)
{
	int i;
	for (i = n/2; i > 0; --i)
		HeapAdjust(s,i,n);
	for (int i = n; i > 1; --i)
	{
		swap(s[1],s[i]);
		HeapAdjust(s,1,i-1);
	}
	
}

2-路归并排序

这是本文的最后一个排序算法,归并排序与上述基于交换、选择等排序的思想不一样,归并的含义是将两个或两个以上的有序表组合成一个新的有序表。
原理:假定待排序列含有n个记录,则可以看作n个有序的子表,每个子表的长度为1,然后两两归并,得到n/2(向上取整)个长度为2或1的子表;再两两归并,…一直重复,直到合并为一个长度为n的有序表为止。

示例图:
归并排序示例图

代码分析:
Merge()函数的功能是将前后相邻的两个有序表归并为一个有序表的算法。设两段有序表s[low…mid]、s[mid+1…high]存放在同一顺序表中的相邻位置上,先将它们复制到辅助数组B中。每次从对应B中的两个段取出一个记录进行关键字的比较,将较小者放入A,当数组B中有一段的下标超过其对应的表长时(即该段的所有元素已经完全复制到A中),将另段的剩余部分直接复制到A中。代码如下:

#define MAXSIZE 100
void Merge(int *list1,int list1_size,int *list2,int list2_size)
{
	int tmp[MAXSIZE];
	int i,j,k;
	i = j = k = 0;
	while(i < list1_size && i < list2_size)//表1,2中都有元素
		tmp[k++] = (list1[i] < list2[i]) ? list1[i++] : list2[i++];

	while(i < list1_size) tmp[k++] = list1[i++];
	while(i < list2_size) tmp[k++] = list2[i++];
	for (j = 0; j < (list1_size + list2_size); ++j)
		list1[m] = tmp[m];
}

函数Merge_Sort()采用递归,代码如下:

void Merge_Sort(int s[],int n)
{
	if(n > 1)//递归跳出条件
	{
		int *list1 = s;
		int list1_size = n/2;
		int *list2 = s + n/2;
		int list2_size = n - list1_size;

		Merge_Sort(list1,list1_size);
		Merge_Sort(list2,list2_size);

		Merge(list1,list1_size,list2,list2_size); 
	}
}

最后对各个排序算法进行总结:

  1. 快速、堆、归并排序的平均时间复杂度为O(nlogn),但是快速排序在最坏情况下为O(n^2)<不如后二者。对于n较大的情况,归并排序速度比堆排序快,但是所需辅助空间更大。
  2. 除希尔排序外的所有插入、起泡、简单选择平均时间复杂度都为O(n^2),直接插入排序最简单,当序列中记录基本有序或n值比较小时,它是最佳算法,因此经常和快速、归并、希尔排序等结合使用。

文章到这里就结束了,如果有写错的地方,欢迎指正!
祝自己复试顺利~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值