数据结构五大类排序(交换类排序 插入类排序 选择类排序 堆排序 归并排序)

一.交换类排序

1.快速排序

1.2算法


void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));//包含在<stdlib>头文件中.

int cmp(const void*a,const void *b) {
	return *(int *)b - *(int*)a; // (b-a 为降序) ( a-b 为升序)
}
qsort(要排序的数组名称,数组中元素的个数,数组中每个元素的大小,回调函数//即 cmp);

1.2算法分析

时 间 复 杂 度 : O ( n l o g 2 n ) 最 坏 时 间 复 杂 度 : O ( n 2 ) 空 间 复 杂 度 : O ( n ∗ l g n ) 平 均 时 间 复 杂 度 是 O ( l o g 2 n ) 稳 定 性 : 排 序 不 稳 定 。 \\ 时间复杂度:O (nlog2^n)\\ 最坏时间复杂度:O(n^2)\\ 空间复杂度:O(n*lgn)\\ 平均时间复杂度是O(log2n)\\ 稳定性:排序不稳定。\\ O(nlog2n)On2OnlgnO(log2n):


2.冒泡排序

1.1基本思想

从第一个元素开始比较相邻的两个元素,如果第一个比第二个大,就互换它们的位置,直到,第n-1项和第n项比较结束,完成第一趟,然后抛弃最大或最小的继续比较。在进行第二趟,第三趟,供需进行n-1趟。

1.2算法

void BubbleSort(int a[], int length)//递增
{
	for (int i = 0; i < length; i++)
	{
		for (int j = 0; j < length-i-1; j++)
		{
			if (a[j+1] < a[j])
				std::swap(a[j], a[j + 1]);
		}
	}
	printf("Finsh sort!\n");
}

1.3算法分析

平 均 时 间 复 杂 度 O ( n 2 ) 平 均 空 间 复 杂 度 : O ( 1 ) 稳 定 性 : 稳 定 平均时间复杂度O(n^2)\\ 平均空间复杂度:O(1)\\ 稳定性:稳定\\ O(n2)O(1):

1.4总结

  1. 递减排序时。只需要改变第7行代码成if (a[j+1] >a[j])即可;

二.插入类排序

1.直接插入法排序

1.1基本思想

将数据分为两组:已排序组,待排序组。每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。

1.2算法

#include <stdio.h>
int main()
{
    int a[] = { 98,76,109,34,67,190,80,12,14,89,1 };
    int k = sizeof(a) / sizeof(a[0]);
    int i, j;
    for (i = 1; i < k; i++)//分组数值下标为0为1组,大于等于1为未排序组
    {
        if (a[i-1]>a[i])
        {
            int temp = a[i];
            for (j = i - 1; j >= 0 && a[j] > temp; j--)  
            {
                a[j + 1] = a[j];
            }
            a[j + 1] = temp;//此处就是a[j+1]=temp;
        }
    }
    for (int f = 0; f < k; f++)
    {
        printf("%d ", a[f]);
    }
}
//法二:
void Insert_Memory(int a[], int length)
{
	for (int i = 1; i < length; i++)
	{
		int j = i - 1;
		int temp = a[i];
		while (temp<a[j]&&j>=0)
		{
			a[j+1] = a[j];
			j--;
		}
		a[j+1] = temp;
	}
}

1.3算法分析

时 间 复 杂 度 O ( n 2 ) 空 间 复 杂 度 : O ( 1 ) 稳 定 性 : 稳 定 时间复杂度O(n^2)\\ 空间复杂度:O(1)\\ 稳定性:稳定\\ O(n2)O(1):

1.4总结

  1. 直接插入法,时间复杂O(n^2)比较高,但是很稳定。
  2. 直接插入法,主要是将数据分组,将待排序组的元素,逐个与已排序组元素比较。需要注意的是,排好一个元素,待排序组区间-1,已排序组区间+1;
  3. 可以用于数组的排序和单链表的排序;

1.5扩展:单链表排序算法

void Init_Sort(Linklist H)
{
	Linklist q,p,r;
	q=H->next->next;
    H->next->next=NULL; //分组
    while(q)
    {
        p=H;   //考虑到,q的位置:要么最小在头节点后面,要么比p大,在p后面。所以这里不能用p=H->next;
        while(p->next&&p->data>q->data)
            p=p->next;
        r=q->next;           //记住未排序的起始                
        q->next=p->next;
        p->next=q;
        q=r;
    }
}

相对,数组的插入排序。链表插入排序的不同之处在于:

  1. 在比较时,未排序元素与表头下一元素开始比较;数组中则是与已排序数据最后一位比较。

2.ShellSort

1.1基本思想

为了解决当数据很多时,自己插入法需要移动次数过多的不足。用Shell法,选定一个增量一般为length/2,增量相同的元素为一组,各组进行直接插入法排序。一轮结束,增量/2,再完成排序。直到增量=1,就形成了比较有序的序列,这样插入排序时,移动次数大大减少。

1.2算法

void ShellSort(int a[], int length)
{
	for (int add=length/2;add>=1;add/=2 )  //增量:add
	{
		for (int i = add; i < length; i++)   //与自己插入法一样,只是区间不同
		{
			int temp = a[i];
			int j = i - add;
			while (temp<a[j]&&j>=0)
			{
				a[j + add] = a[j];
				j = j - add;
			}
			a[j + add] = temp;   //找到合适位置,插入
		}
	}

}

1.3算法分析

时 间 复 杂 度 O ( n 1.3 — 2 ) 空 间 复 杂 度 : O ( 1 ) 稳 定 性 : 不 稳 定 时间复杂度O(n^{1.3—2})\\ 空间复杂度:O(1)\\ 稳定性:不稳定\\ O(n1.32)O(1):

1.4总结:

ShellSort是直接插入法排序的更高效的改进版本。其实,无论用哪一种排序,都可以先做一次ShellSort。


3.BinarySort

1.1基本思想

通过折半查找,快速锁定插入的位置

1.2算法:

void ByInsertSort(int a[], int length)   //折半插入排序
{
	for (int i = 1; i < length; i++)
	{
		int j,m;
		int temp = a[i];
		int low = 0;
		int high=i-1;
		while (low<=high)
		{
			 m = (low + high) / 2;
			if (a[m] > temp)high =m-1;
			else low=m+1;
		}
		for (j = i; j >high; j--)
		{
			a[j] = a[j - 1];
		}
		a[high+1] = temp;   //high+1或low都行

	}
}

1.3算法分析

时 间 复 杂 度 O ( n 2 ) 空 间 复 杂 度 : O ( 1 ) 稳 定 性 : 稳 定 不 能 用 于 链 式 结 构 , 适 用 于 顺 序 表 , n 较 大 的 情 况 时间复杂度O(n^2)\\ 空间复杂度:O(1)\\ 稳定性:稳定\\ 不能用于链式结构,适用于顺序表,n较大的情况 O(n2)O(1):n


三.选择排序类

1.简单选择排序:

1.1基本思想

​ 每一趟从未排序列中选出最小的一个放到已排序列中。

1.2算法:

//简单选择排序
void SelectionSort(int a[], int length)
{
	for (int i = 0; i < length; i++)
	{
		for (int j = i + 1; j < length; j++)
		{
			if (a[i] > a[j])std::swap(a[i], a[j]);
		}
	}
}

1.3算法分析

时 间 复 杂 度 O ( n 2 ) 空 间 复 杂 度 : O ( 1 ) 稳 定 性 : 不 稳 定 能 用 于 链 式 结 构 , 移 动 记 录 次 数 少 , 当 每 一 记 录 较 大 时 , 该 方 法 比 直 接 插 入 发 快 。 时间复杂度O(n^2)\\ 空间复杂度:O(1)\\ 稳定性:不稳定\\ 能用于链式结构,移动记录次数少,当每一记录较大时,该方法比直接插入发快。 O(n2)O(1):

2.堆排序

2.1基本思路:

把数组看作一颗二叉树。从倒数第二排开始,作根节点与两个子节点的比较,该根节点下标应是:i=(n-2)/2,把最大的节点与根节点交换。接着下一个根节点的下标应该是为:i-1。到倒数第三排时,应该注意的是,如下图:

这时,0作为根节点比1和三要小,所以每当我们排好一个根节点后,需要再排交换下来的那一个节点作为根节点的二叉树。

在这里插入图片描述

​ 如上图,右边最终结果已经是为最大堆。那么该如何把它输出?

​ 我们把,根节点和最后一个节点交换,再把最后一个元素剔除,这样剔除的元素就是最大的元素,而且下标也是最大下标。但是这时,交换元素后,根节点不是最大的节点,需对根节点做一次排序。这样,每一次剔除,就要对根节点做一次排序。

2.2算法:

//堆排序
void HeapSort(int a[], int length)
{
	BuidHeap(a, length);
	int i = length - 1;
	for (i; i >= 0; i--)
	{
		std::swap(a[0], a[i]);
		Heap(a, 0,i);
	}
}

//大顶堆的排序
void Heap(int a[], int i,int n)
{
	int Lc = 2*i+1;
	int Rc = 2 * i + 2;
	int temp = i;
	if (Lc<n && a[Lc]>a[temp])temp = Lc;
	if (Rc<n && a[Rc]>a[temp])temp = Rc;
	if (i != temp)
	{
		std::swap(a[i], a[temp]);
		Heap(a, temp, n);
	}
}

//创建大顶堆
void BuidHeap(int a[], int length)
{
	int parents = (length - 2)/2;
	for (int i = parents; i >= 0; i--)
	{
		Heap(a, i, length);
	}
}

2.3算法分析:

平 均 时 间 复 杂 度 O ( n l g n ) ( 接 近 最 坏 性 能 ) 空 间 复 杂 度 : O ( 1 ) 稳 定 性 : 不 稳 定 不 能 用 于 链 式 结 构 , 当 记 录 较 多 是 比 较 合 适 平均时间复杂度O(nlgn)(接近最坏性能)\\ 空间复杂度:O(1)\\ 稳定性:不稳定\\ 不能用于链式结构,当记录较多是比较合适 O(nlgn)O(1):

四:归并排序

1.基本思想:

​ 假设有一数组A:{4,5,6,7,1,2,3,4}。我们把数值A分为两有序组:{4,5,6}和{1,2,3}然后把这两个组归并,形成{1,2,3,4,5,6};那么当分的两组无序时,例如{8,4,5,7,1,3,6,2}则我们把它分到每两组为单个元素。

1.2算法

void Merge(int a[],int L, int M, int R)//合并
{
	int i, j, k;
	i = L; j = M + 1; k = 0;  //i是第一组的开通,j是第二组的开头
	int* b = (int*)malloc(sizeof(int) * (R-L));//分配内存大小与两组大内存小相同的b数组
	while (i<=M&&j<=R)  
	{
		if (a[i] < a[j]) {
			b[k++] = a[i++];
		}
		else
		{
			b[k++] = a[j++];
		}
	}
	while (i<=M)
	{
		b[k++] = a[i++];
	}
	while (j<=R)
	{
		b[k++] = a[j++];
	}
	for ( i = R; i >= L; i--)
	{
		a[i] = b[--k];
	}
}

//
void MergeSort(int a[], int L, int R)   //分组
{
	int M;
	if (R == L)return;  //当R==L时。也就是两组只有一个元素时。
	else
	{
		M = (R + L) / 2;
		MergeSort(a, L, M); //分第一组,以L为开头,M为尾
		MergeSort(a, M + 1, R);//第二组,从M+1为开头,R为结尾。
		Merge(a, L, M, R);//合并两组。
	}
}

1.3算法分析

时 间 复 杂 度 O ( n l o g 2 n ) ( 接 近 最 坏 性 能 ) 空 间 复 杂 度 : O ( n ) 稳 定 性 : 稳 定 用 于 链 式 结 构 , 不 需 要 额 外 的 存 储 空 间 , 但 递 归 实 现 时 , 任 然 需 要 开 辟 相 应 的 递 归 工 作 栈 时间复杂度O(nlog2^n)(接近最坏性能)\\ 空间复杂度:O(n)\\ 稳定性:稳定\\ 用于链式结构,不需要额外的存储空间,但递归实现时,任然需要开辟相应的递归工作栈 O(nlog2n)O(n):

五.基数排序

1.基本思想:

作一数组大小为9,称为桶。先把要排数组,按照个位数的大小排序。统计每种个位数的元素有几个,而后再用当前个桶值加前一个桶值得到最终该桶的值。如:

此时刷新待排数值的序列为上面按照个位数排好的序列

在按照十位数,排序。

此时,右边序列就是排序好的序列。

那么桶的值有什么用呢?就以上图为例。

​ 按照图1从后开始,9对应桶的值为4。图二对应排序好的9的下标为3。按此规律,满足桶的值减一等于元素在数值中对应的下标。

2.算法

void RadixSort(int a[], int length)
{
	int base = 1;
	int M=a[0];
	for (int i = 1; i < length; i++)  //选出最大数,求最大数有几位
	{
		if (M < a[i])M = a[i];
	}
	int bucket[10] = { 0 };
	int* b = (int*)malloc(sizeof(int) * max);
	while (M / base >0) //当超过最大位数时,结束
	{
		
		for (int i = 0; i < length; i++)
		{
			bucket[a[i] / base % 10]++;        //给桶赋值
		}
		for (int i = 1; i < 10; i++)
		{
			bucket[i] += bucket[i - 1];        //桶最终赋值
		}
		for (int i = 0; i < length; i++)
		{
			b[bucket[a[i] / base % 10]-1] = a[i];   //把排序序列存在b中
			bucket[a[i] / base % 10]--;
		}
		for (int i = 0; i < length; i++)
		{
			a[i] = b[i];
		}
		base =base * 10;
	}

}

3.算法分析

时 间 复 杂 度 O ( d ( n + r d ) ) 空 间 复 杂 度 : O ( n + r d ) 稳 定 性 : 稳 定 可 以 用 于 链 式 结 构 , 需 要 知 道 各 级 关 键 字 的 主 次 关 系 和 取 值 范 围 时间复杂度O(d(n+rd))\\ 空间复杂度:O(n+rd)\\ 稳定性:稳定\\ 可以用于链式结构,需要知道各级关键字的主次关系和取值范围 O(d(n+rd))O(n+rd):

六总结:

总的来看, 各种排序算法各有优缺点,没有哪一 种是绝对最优的。在使用时需根据不同情况适当选用,甚至可将多种方法结合起来使用。

一****般综合考虑以下因素:

**(I)**待排序的记录个数;

(2) 记录本身的大小;

**(3)**关键字的结构及初始状态;

(4) 对排序稳定性的要求;

(5) 存储结构。

根据这些因素和表8.2所做的比较,可以得出以下几点结论。

**(I)**当待排序的记录个数n较小时,n^2 和nlog2n的差别不大, 可选用简单的排序方法。 而当关键字基本有序时, 可选用直接插入排序或冒泡排序,排序速度很快, 其中直接插入排序最为简单常用、性能最佳。

**(2)****当n较大时,应该选用先进的排序方法。对于先进的排序方法, 从平均时间性能而言,快速排序最佳,是目前基于比较的排序方法中最好的方法。但在最坏情况下,即当关键字基本有序时,快速排序的递归深度为n, 时间复杂度为O(n2) 空间复杂度为O(n)。 堆排序和归并排序不会出现快速排序的最坏情况,但归并排序的辅助空间较大。 这样, 当n较大时, 具体选用的原则是:**当关键字分布随机, 稳定性不做要求时, 可采用快速排序;

@当关键字基本有序, 稳定性不做要求时, 可采用堆排序;

@当关键字基本有序, 内存允许且要求排序稳定时 可采用归并排序。

(3) 可以将简单的排序方法和先进的排序方法结合使用。 例如, 当n较大时,可以先将待排序序列划分成若干子序列, 分别 进行直接插入排序, 然后再利用归并排序,将有序子序列合并成一个完整的有序序列。 或者, 在快速排序中, 当划分子区间的长度小于某值时, 可以转而调用直接插入排序算法。

**(4)**基数排序的时间复杂度也可写成 O(d·n)。 因此,它最适用千n值很大而关键字较小的序列。若关键字也很大, 而序列中大多数记录的 “ 最高位关键字“ 均不同, 则亦可先按 “ 最高位关键字“ 不同将序列分成若干 “ 小 ” 的子序列, 而后进行直接插入排序。 但 基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值范围, 即只适用于像整数和字符这类有明显结构特征的 关键字, 当关键字的取值范围为无穷集合时,则无法使用基数排序。

**(5)**从方法的稳定性来比 较,基数排序是稳定的内排方法, 所 有时间复杂度为O(n2 )的简单排序法也是稳定 的 , 然而, 快速排序、堆排序和希尔排序等时间性 能较好的排序方法都是不稳定的。

一般来说, 如果排序过程中的 “比较” 是在 ”相邻的两个记录关键字“ 间 进行的,则排序方法是稳定的。 值得提出的是,稳定性是由方法本身决定的,对不稳定的排序方法而言,不管其描述形式如何,总能举出一个说明不稳定的实例来。反之,对稳定的排序方法, 可能有的描述形式会引起不稳定,但总能找到一种不引起不稳定的描述形式。 由千大多数情况下排序是按记录的主关键字进行的,则所用的排序方法是否稳定无关紧要。若排序按记录的次关键字进行,则必须采用稳定的排序方法。

用千n值很大而关键字较小的序列。若关键字也很大, 而序列中大多数记录的 “ 最高位关键字“ 均不同, 则亦可先按 “ 最高位关键字“ 不同将序列分成若干 “ 小 ” 的子序列, 而后进行直接插入排序。 但 基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值范围, 即只适用于像整数和字符这类有明显结构特征的 关键字, 当关键字的取值范围为无穷集合时,则无法使用基数排序。**

**(5)**从方法的稳定性来比 较,基数排序是稳定的内排方法, 所 有时间复杂度为O(n2 )的简单排序法也是稳定 的 , 然而, 快速排序、堆排序和希尔排序等时间性 能较好的排序方法都是不稳定的。

一般来说, 如果排序过程中的 “比较” 是在 ”相邻的两个记录关键字“ 间 进行的,则排序方法是稳定的。 值得提出的是,稳定性是由方法本身决定的,对不稳定的排序方法而言,不管其描述形式如何,总能举出一个说明不稳定的实例来。反之,对稳定的排序方法, 可能有的描述形式会引起不稳定,但总能找到一种不引起不稳定的描述形式。 由千大多数情况下排序是按记录的主关键字进行的,则所用的排序方法是否稳定无关紧要。若排序按记录的次关键字进行,则必须采用稳定的排序方法。

参考文献:《数据结构严蔚敏第二版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值