当前位置: 首页 > article >正文

八大排序——万字长文带你剖析八大排序(C语言)

        本篇文章主要介绍八大排序的思想和具体实现,也会分析具体的时间复杂度和空间复杂度,提醒一些容易出现的坑和实现一些不同版本的排序,以及这些不同排序之间的效率分析

目录

1.插入排序

1.1直接插入排序

1.1.1 直接插入排序的思想:

1.1.2 直接插入排序的复杂度分析:

2.1希尔排序

2.1.1希尔排序的思想:

2.1.2 希尔排序的时间复杂度问题

2.选择排序

2.1 选择排序

选择排序的时间复杂度

2.2 堆排序

向上建堆时间复杂度分析

向下建堆时间复杂度分析

堆排序的思想和实现:

 堆排序的时间复杂度分析

3.交换排序

3.1 冒泡排序

3.2 快速排序

快速排序                --hoare版本

快速排序                --挖坑法

快速排序                --前后指针法 

快速排序的非递归版本 

快速排序的三数取中和小区间优化

4.归并排序

4.1 归并排序的非递归版本

归并排序的复杂度分析

5.非比较排序

5.1 计数排序

6. 测试性能


1.插入排序

1.1直接插入排序

1.1.1 直接插入排序的思想

        若要排升序,就跟前面的数比较,我比你小,我就插入到你的位置,你就往后挪,然后继续跟前面的数比较,我比你大,那这一趟排序就走完了。

        另一种情况是我一直往前插入,插入到0下表的位置,继续往前走,走到-1了,这时候就说明单趟排序已经走完了。

        当我从n-1位置开始走就是最后一趟排序,直接插入排序就完成了

每趟排序的过程是向数组下表0位置去走的,而每趟排序的开始位置是一直往数组尾部去变的,比如我开始比较下表1和下表0的位置,下表1的元素比下表0小,那你就向后挪,我再跟前面的比,前面的是-1所以这趟排序就走完了。

        这时候[0,1]位置就是已经排好序的区间,下一次来排序就是在一个有序的区间内去进行插入

       具体实现的时候先实现单趟排序的逻辑,把思路理顺,再去控制多趟就轻松许多了~~

我们先进行单趟的讲解

首先我们先定一个已经有序的区间,[0,end],而end因为数组下表0位置的数只有一个所以不需要比较,所以end此时给值0,我们从end+1的位置跟end区间进行比较。

上面分析过了,当我比你小或者我走到-1时,单趟就走完了

而我已经插入到[0,end]这个区间,所以end需要++。因为比较是从end下一个位置开始比较的,你不更新区间就会死循环。

而多趟排序我们用循环控制,当end < n-1时,也就是end最多是倒数第二个元素,为什么?因为我们是从end+1位置走的,当end到倒数第二个位置,实际我们已经从最后一个元素开始向前插入了,也就是最后一趟排序已经开始了,如果end到了最后一个元素那么就越界了

//直接插入排序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//单次插入的过程
		int end = i;
		//从end+1开始比较,一直到n-1位置停止
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];//向后挪动
				end--;
			}
			else
			{
				break;
			}
		}
		//循环结束,end=-1,插入在下表为0的位置
		//break跳出,end+1位置大于end的位置,在end+1的位置放入tmp
		a[end + 1] = tmp;
	}
	
}

1.1.2 直接插入排序的复杂度分析

假设数组元素是n个,那么单趟排序的个数就是:

第一次排序一个元素

第二次排序两个元素

...

最后一次排序n-1个元素

总共的排序次数:1 + 2 + 3 + 4......+ n-3 + n-2 + n-1

是一个等差数列,套用公式可以发现

总共的排序次数 = (n-1)* n /2  = (n^2 -n)/2

时间复杂度就是:O(N^2)

当然直接插入排序是有序的情况下,时间复杂度是O(N),因为已经有序就只需要比较一个数就可以

而因为没有使用额外的空间,所以空间复杂度是O(1)

2.1希尔排序

希尔排序是有大佬根据直接插入排序的思想上进行改良,从而得到的一个排序,是谁不用多说,希尔排序那肯定是希尔发明的

2.1.1希尔排序的思想:

希尔排序引入了一个预排序的概念,先排gap组的数据,然后再进行直接插入排序。

首先我们知道当数组接近有序的时候,直接插入排序的时间复杂度是很低的,而先用gap进行预排序,就可以让小的数据更快的往前挪,大的数据更快的往后挪,从而让数据更接近有序

假设gap是3,那么就不是从end位置的后一个开始依次往前排了,而是从end+gap的位置开始往前排,每次走gap步。

也就是说从直接插入排序单趟要排序一个连续的区间,变成了要排序一个集合{0,0+gap,0+2*gap....0+n*gap}

而end的结束位置也从end-2变成了end-gap-1

当走完第一次单趟,下一次就是从下表1开始走,一共走gap趟预排序就走完了

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;//这里进行多趟预排序
	while (gap > 1)
	{
		gap = gap /3  + 1;//每次都缩小预排序的范围,gap=1就是直接插入排序
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];//从end+gap开始排序,从n-gap-1下表结束这趟排序
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

可以看跟直接插入排序的算法很像,但是其中思想要搞明白

2.1.2 希尔排序的时间复杂度问题

希尔排序的时间复杂度的计算很有难度,这里只能尝试计算个大概的范围,当然有人计算过希尔排序的时间复杂度大约在 O(N^1.3)

假设数组元素是n

第一趟排序: gap= n/3,这里忽略掉+1影响不大。那么每组的个数就是3个。

总共要比较的次数按照每组比较次数最坏情况来算就是:(1+2)*n/3 = n

第二趟排序:gap = n/9,每组个数9个

总共要比较的次数按照每组比较次数最坏情况来算就是:(1+2+3....+8)*gap/9 = 4n

但是经过了第一趟的预排序,此时每组的比较次数已经不可能是最坏情况了,所以这里的总共的次数比4n要小

最后一趟排序:gap = 1,每组数据是n个,此时数组非常接近有序,gap=1就是直接插入排序,而直接插入排序在数组已经很接近有序的情况下时间复杂度是O(N)

由此可以看到,希尔排序的时间复杂度从开始到结束,是一个从n开始递增后又递减的,然后又到n的区间

2.选择排序

2.1 选择排序

思想:若要排升序,那么从begin+1下表位置遍历数组选一个最小的数跟end位置交换,选一个最大的元素跟begin位置交换

判断begin是不是就是最大的值,如果是,那么就在begin跟mini交换后,将maxi的值改为mini

代码:

//选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n-1;
	int maxi = 0;
	int mini = 0;
	while (begin < end)
	{
		for (int i = begin+1; i <= end; i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
			Swap(&(a[begin]), &(a[mini]));
			if (begin == maxi)
				maxi = mini;
			Swap(&(a[end]), &(a[maxi]));

		++begin;
		--end;
		mini = begin;
		maxi = begin;
	}

2.1.1 选择排序的时间复杂度

数组元素n个,第一次要遍历n-1次,第二次遍历n-2,可以得出总的时间复杂度为n-1+n-2+n-3+.....2+1。套用求和公式就是

n(n-1)/2 = (n^2 -n)/2

实际时间复杂度就是:O(N)

2.2 堆排序

堆排序就是利用堆的父结点一定比它的左右子树的孩子大的特性,每次将堆顶最大或最小的数跟最后一个元素交换,然后从堆顶开始向下调整,这样一个结点就排序好了。

当然,堆不是凭空出现的,要进行排序就先要建堆。

建堆分两种,向上调整建堆和向下调整建堆

向上调整建堆就是,每次尾插到数组,然后从最后一个结点向上调整建堆。

向下调整建堆就是,从二叉树的最后一个父节点开始,向下调整建堆。因为堆都是左右子树小于或大于父节点的,所以从最后一个父节点开始向下调整,因为是数组,所以下表--就可以得到所有的父节点,当走到根向下调整完了之后,堆就建好了

2.2.1 向上建堆时间复杂度分析

首先向上调整从最后一个结点向上调整建堆:

总的比较次数 = 每一层比较次数*每一层的结点个数

T(h) = (h-1)*2^(h-1)+ (h-2)*2^(h-2)+......+2*2^2 + 1*2^1

2T(h) = (h-1)*2^(h)+ (h-2)*2^(h-1)+......+2*2^3 + 1*2^2

这里忽略负数

T(h)  = (h-1)*2^(h) + 2^(h-1)+2^(h-2)+......+2^2+2^1+2^0-1

T(h) = log(n-1) * N -1

可以得到向上建堆的时间复杂度就是O(N*logN)

2.2.2 向下建堆时间复杂度分析

总的比较次数 = 每一层比较次数*每一层的结点个数

T(h) = 1*2^(h-2) + 2*2^(h-3)+ 3*2^(h-4).....+(h-3)*2^2 + (h-2)*2^1 + (h-1)*2^0

2T(h) = 1*2^(h-1) + 2*2^(h-2)+ 3*2^(h-3).....+(h-3)*2^3 + (h-2)*2^2 + (h-1)*2^1

T(h) = 2^(h-1)+2^(h-2)+2^(h-3).....+2^2+2^1+2^0-h

T(h) = 2^h-1-h = N-logn-1

可以得到向下建堆的时间复杂度就是O(N)

2.2.3 堆排序的思想和实现

堆排序最重要的就是向下调整的思想。若是要建大堆,那么从根结点开始,找到左右孩子中大的那一个,更它比较,若父节点比孩子结点大,那么向下调整就结束。如果父节点比孩子结点小,那么就交换它们的值,再将孩子结点给给父亲,再找更新后的父亲结点的左右孩子结点当中大的那个,继续进行比较,直到孩子结点不存在就结束

void AdjustDown(int* array, int n, int size)
{
	assert(array);
	int parent = n;
	int child = n * 2 + 1;//首先假设法左孩子是左右孩子结点当中大的那个
	if (child + 1 < size && array[child] < array[child + 1])
	{
        //当左孩子小于右孩子,那就说明右孩子大,就更新孩子结点
		child = n * 2 + 2;
	}

	while (child < size)//当子节点不存在时结束
	{
		if (array[child] > array[parent])
		{
			Swap(&array[child], &array[parent]);
			parent = child;
			child = parent * 2 + 1;
            //继续找出左右孩子结点中大的那个
			if (child + 1 < size && array[child] < array[child + 1]) 
			{
				child = parent * 2 + 2;
			}
		}
		else
		{
			break;
		}
	}
}

堆排序的实现:

//堆排序
void HeapSort(int* a, int n)
{
	int parent = (n - 1 - 1) / 2;
	while (parent >= 0)
	{
		AdjustDown(a, parent, n);
		parent--;
	}
	for (int i = n - 1; i > 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, 0, i);
	}
}

 2.2.4堆排序的时间复杂度分析

建堆的时间复杂度是O(N),排序的时间复杂度是N*(logN)

空间复杂度是O(1),因为是对数组排序,所以可以在数组中进行建堆

3.交换排序

3.1 冒泡排序

冒泡排序假设排升序,那么从下表0开始,依次跟后一个比较,我比你大就交换,我比你小那我就不交换。不管叫不交换,我都向后走一位,当我走到最后一个元素时就停下来。

void BubbleSort(int* a, int n)
{
	for(int j = 0; j < n - 1; j++)
	{
		for (int i = 0; i < n - 1 -j; i++)
		{
			if (a[i] > a[i + 1])
			{
				Swap(&a[i], &a[i + 1]);
			}
		}
	}
	
}

冒泡排序的复杂度:

空间复杂度:O(1),因为没有使用额外的空间

时间复杂度:O(N^2),总共比较的次数是一个等差数列,所以时间复杂度是

3.2 快速排序

快速排序是hoare大佬发明的一种排序,主要的逻辑就是,从最左边选key,然后让右边先走

右边先走,找到比key小的就停下来,换左边就开始找比key大的,找到比key大的就停下来交换它们,然后右边继续找比key小的;直到它们相遇,就交换相遇位置和key的位置。一趟排序就可以确定一个元素最终的位置。

假设左边从L开始,右边从R开始

当L遇到R时:当L开始走的时候,R因为是先走的,所以一定是找到比key小的了,所以当它们相遇的时候一定是比key小的位置。

当R遇到L时:当R开始走,一直找没找到,L跟R相遇了,这时有两种情况。

第一种情况是,L的位置是上一次R找到的比key小的位置,因为每一次都会交换。

第二种情况:L一直没动,R一直找小没找到,直到跟L相遇,那么就结束了,key的位置跟L相同,所以也是没问题的

3.2.1 快速排序        --hoare版本

快速排序可以通过递归分解子问题来进行解决,因为每次要排序的就是一个区间,当将key位置排好序之后,就可以继续递归走key的左右区间。

采用前序遍历就可以很好的解决递归的问题

先进行整个数组的排序,然后找出key,将参数的left和keyi-1当作左子树递归,将keyi+1和right当做右子树递归。

返回条件:当left >=  right就说明区间不存在或只有一个元素,就不需要进行排序了

//快速排序  --hoare版本
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begini = left;
	int endi = right;
	int keyi = left;

	while (begini < endi)
	{
		while (begini < endi && a[endi] >= a[keyi])
		{
			endi--;
		}
		while (begini < endi && a[begini] <= a[keyi])
		{
			begini++;
		}
		Swap(&a[begini], &a[endi]);
	}
	Swap(&a[begini], &a[keyi]);
	keyi = begini;
	//[left, keyi-1] keyi [keyi+1, right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

3.2.2 快速排序    --挖坑法

有人在hoare的版本之后提出了一种挖坑法来进行单趟排序

首先取最左边的值存放在一个key里,然后L指向坑位。从R开始找比key小的值,找到了就将R找到位置的值放入L指向的位置也就是坑位中,这时R指向的位置就变成了坑位,然后L开始找比key大的值,找到就放到坑位中,当它们相遇时,就一定是在坑位相遇,因为只有当L或R的其中一个指向新的坑位时另一个才会继续走,此时就把key放入坑位中。

//快速排序  --挖坑法
void TrenchiQuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begini = left;
	int endi = right;
	int key = a[left];
	int holei = left;

	while (begini < endi)
	{
		while (begini < endi && a[endi] >= key)
		{
			endi--;
		}
		a[holei] = a[endi];
		holei = endi;
		while (begini < endi && a[begini] <= key)
		{
			begini++;
		}
		a[holei] = a[begini];
		holei = begini;
	}
	a[holei] = key;
	
	//[left, keyi-1] keyi [keyi+1, right]
	TrenchiQuickSort(a, left, holei - 1);
	TrenchiQuickSort(a, holei + 1, right);
}

3.2.3 快速排序     --前后指针法 

前后指针法是一直更简单的方法。

还是取最左边的元素做key。

指针cur指向key位置的下一个位置,prev指向key。

cur一直往后走,当cur的值比key大,prev就不动,当cur的值比key小,那prev++,再交换cur和prev位置的值。当cur大于数组有效元素时就停下来,将key跟prev交换

也就是说,如果cur没有找到比key小的值,那么prev就指向key。

//快速排序  --前后指针法
void PrevCurPQuickSort(int* a,int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int previ = left;
	int curi = previ + 1;
	int keyi = left;
	while (curi <= right)
	{
		while (curi <= right && a[curi] < a[keyi])
		{
            //当cur的位置小于key就一直循环,交换prev和cur的值
			previ++;
			Swap(&a[curi], &a[previ]);
			curi++;
		}
        //当结束循环时,那么cur此时的位置是一定大于key的值或者已经不存在的
		curi++;
	}
	Swap(&a[keyi], &a[previ]);
	keyi = previ;
	//[left, keyi-1] keyi [keyi+1, right]
	PrevCurPQuickSort(a, left, keyi - 1);
	PrevCurPQuickSort(a, keyi + 1, right);
}

3.2.4 快速排序的非递归版本 

有的时候你递归的深度太深是有栈溢出的风险的,因为在操作系统里栈是很小的,而堆是很大的。所以我们在堆上申请一段空间来模拟递归的过程,这就是非递归

我们用栈来模拟快排递归的过程,首先先将原数组的left和right入栈,然后进行快速排序,这里用的是hoare版本的快排,选最左边为key,让右边先走找比key小的值,找到了那左边就开始走找比key大的值,当找到的时候就交换它们,然后右边继续找比key小的值,如果相遇了,那么相遇的位置就一定是比key小的值,那么就交换相遇位置和key的位置。

因为是非递归,模拟递归的过程,将key的右区间先入栈,在将左区间入栈。

因为栈是后入先出,所以先出栈的是左区间,再进行左区间的单趟快速排序,然后再划分区间入栈

入栈时判断模拟实际递归返回条件,当左区间不存在或者区间只有一个元素就不入栈

当栈为空时说明数组已经排序成功

//快速排序  --非递归版本
void NonRecursiveQuickSort(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);
	while (!StackEmpty(&st))
	{
		int begini = StackTop(&st);
		StackPop(&st);
		int endi = StackTop(&st);
		StackPop(&st);
		int left = begini;
		int right = endi;
		int keyi = begini;
		while (begini < endi)
		{
			while (begini < endi && a[endi] >= a[keyi])
			{
				endi--;
			}
			while (begini < endi &&  a[begini] <= a[keyi])
			{
				begini++;
			}
			Swap(&a[endi], &a[begini]);
		}
		Swap(&a[keyi], &a[begini]);
		keyi = begini;
		// [left, keyi -1]  keyi [keyi+1, right]
        if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}
		if (left < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}	
	}
	StackDestory(&st);
}

3.2.5 快速排序的三数取中和小区间优化

快速排序有这样一中极端情况,当每次取到的key都是数组最小的值,那么时间复杂度就会变慢,这时候我们就需要来让快排稳定点。

还有一种情况,当快排递归时的区间只有几个元素,那就不用递归了,因为递归也是需要资源的,函数开辟栈帧

三数取中顾名思义就是在三个数中取中间数来做key,当然这三个数你可以选随机数或者最左边最右边和中间,这里采用的是第二种方法

小区间优化则是当递归到区间内只有几个元素时,那么可以采用直接插入法,来让区间有序。

//快速排序  --非递归版本
void NonRecursiveQuickSort(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);
	while (!StackEmpty(&st))
	{
		int begini = StackTop(&st);
		StackPop(&st);
		int endi = StackTop(&st);
		StackPop(&st);
		int left = begini;
		int right = endi;
		int keyi = begini;
		while (begini < endi)
		{
			while (begini < endi && a[endi] >= a[keyi])
			{
				endi--;
			}
			while (begini < endi &&  a[begini] <= a[keyi])
			{
				begini++;
			}
			Swap(&a[endi], &a[begini]);
		}
		Swap(&a[keyi], &a[begini]);
		keyi = begini;
		// [left, keyi -1]  keyi [keyi+1, right]
		if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}
		if (left < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}
	StackDestory(&st);
}

快排的时间复杂度一般认为是O(N*logN)。

空间复杂度递归版本因为有递归,而递归的层数类似二叉树的高度,而非递归版本又开了一个栈,栈也要保证能存高度个

所以空间复杂度是(logN)

4.归并排序

归并排序就是开一个跟原数组大小相同的数组tmp,然后从原数组的中间位置,将左区间和右区间当作有序的,利用tmp来将两个有序数组排序。

利用后序遍历的分治思想,来进行归并排序

首先先让函数递归,算出中间位置,递归左区间和右区间,当左右区间不存在或者左右区间或左右区间只有一个元素时就返回上一层递归

此时左区间只有一个元素,右区间也只有一个元素,所以下一层递归已经返回了,此时认为左右区间都有序,就对两个有序数组开始排序,排序结束,返回上一层后。

此时左区间返回了有序的区间,右区间也返回了有序的区间,而我就又开始对两个有序数组进行排序,直到最后一层排序完成,整个数组的排序就完成了

//归并排序
void _MergeSort(int* a, int* tmp, int left, int right)
{
	if (left >= right)
		return;
	int mid = (left + right) / 2;
	_MergeSort(a, tmp, left, mid);
	_MergeSort(a, tmp, mid + 1, right);
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i] = a[begin1];
			begin1++;
			i++;
		}
		else
		{
			tmp[i] = a[begin2];
			i++;
			begin2++;
		}
	}
	while(begin1 <= end1)
	{
		tmp[i] = a[begin1];
		begin1++;
		i++;
	}
	while (begin2 <= end2)
	{
		tmp[i] = a[begin2];
		i++;
		begin2++;
	}
	memcpy(a+left, tmp+left, sizeof(int)*(right - left + 1));
}
void MergeSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int* tmp = (int*)malloc(sizeof(int) * (right - left + 1));
	assert(tmp);
	_MergeSort(a, tmp, left, right);
	free(tmp);
}

4.1 归并排序的非递归版本

实际就是利用循环来进行模拟递归的过程。因为是后序遍历所以,第一次排序的左右区间只有两个元素,所以我们利用循环来进行排序,将最开始需要排序的区间设为gap,每次排gap个,i+=gap,当i大于right的时候就结束。然后gap每趟排序完之后*=2。

当然还会有问题,当gap很大,求中间位置的下表时会乘以2倍的gap,就可能会导致越界

越界有三种情况:

        end1  begin2  end2  越界

        begin2 end2 越界

        只有end2越界了

当我们可以处理begin2越界的情况,因为有两种越界都有begin2,而begin2越界说明右区间根本就不存在,而左区间又是一个有序的区间,所以就可以当作已经排序完成,直接返回即可

当只有end2越界了,那么说明右区间是有值的,但是end2又越界了,我们直接将前面记录的right给给end2。

//归并排序非递归版本
void NonRecursiveMergeSort(int* a, int left, int right)
{
	int* tmp = (int*)malloc(sizeof(int) * (right - left + 1));
	int gap = 1;
	
	while (gap < right)
	{
		for (int i = 0; i < right; i += 2 * gap)
		{
			int mid = (i + i + 2 * gap - 1) / 2;
			int begin1 = i, end1 = mid;
			int begin2 = mid + 1, end2 = i + 2 * gap - 1;
			int j = begin1;
			if (begin2 > right)
			{
				break;
			}
			if (end2 > right)
			{
				end2 = right;
			}
			//printf("[%d %d] [%d %d] ",begin1, end1, begin2, end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j] = a[begin1];
					j++;
					begin1++;
				}
				else
				{
					tmp[j] = a[begin2];
					begin2++;
					j++;
				}
			}
			while (begin1 <= end1)
			{
				tmp[j] = a[begin1];
				j++;
				begin1++;
			}
			while (begin2 <= end2)
			{
				tmp[j] = a[begin2];
				begin2++;
				j++;
			}
			//printf("%d",i);
			memcpy(a + i, tmp + i, sizeof(int) * (end2- i+1));
		}
		gap *= 2;
		//printf("\n");
	}
	free(tmp);
}

4.2 归并排序的复杂度分析

 归并排序首先要开辟一个n个元素的数组,所以空间复杂度是O(N)

而归并排序的时间复杂度,因为是取中间位置进行递归,所以归并排序像是一颗完全二叉树,所以归并排序的时间复杂度就是严格的O(N*longN)

5.非比较排序

5.1 计数排序

计数排序就是先找到数组内最小的和最大的数,开辟它们差值的数组count初始化所有元素为0,然后遍历原数组将原数组的元素-最小值当作count数组的下表,将该下表位置的值++,遍历完原数组。

遍历count数组,遇到不为0的元素就--该元素,然后往原数组尾插count该位置的下表+最小值,当count数组该下表位置的值为0时,就遍历下一个元素,直到遍历结束,count数组此时的值都为0,而原数组已经排好序了

//计数排序
void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	int j = 0;
	for (int i = 1; i < n; i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (min > a[i])
		{
			min = a[i];
		}
	}
	int* countArr = (int*)calloc((max - min + 1),sizeof(int));
	for (int i = 0; i < n; i++)
	{
		countArr[a[i] - min]++;
	}
	for (int i = 0; i < max - min + 1; i++)
	{
		while (countArr[i]--)
		{
			a[j] = i + min;
			j++;
		}
	}

}

计数排序的时间复杂度是O(N),但是计数排序只能排整数,且计数排序只适合最大值和最小值差距不是很巨大的数据

6. 测试性能

最后我们来测试一些这八大排序的性能

首先我们来上十万个数来小试牛刀

十万数据

可以看到啊,三个O(N^2)的排序跟O(N*logN)的是不是没有可比性啊,冒泡为什么这么慢......

直接插入也是O(N^2)里的大哥了

接下来我们屏蔽掉三个O(N^2)的排序,来进行O(N*logN)的战斗把!

一千万数据走起!

可以看到哈,这些排序基本都是一个数量级的。当然计数排序这么突出也是有原因的,因为O(N)的时间复杂度真的可以为所欲为。

void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);


	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];
	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a3, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	QuickSort(a5, 0, N - 1);
	int end5 = clock();

	int begin6 = clock();
	MergeSort(a6, 0, N - 1);
	int end6 = clock();

	int begin7 = clock();
	BubbleSort(a7, N);
	int end7 = clock();

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = clock();
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("QuickSort:%d\n", end5 - begin5);
	printf("MergeSort:%d\n", end6 - begin6);
	printf("BubbleSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
}


http://www.kler.cn/a/318302.html

相关文章:

  • 【OpenEuler】配置虚拟ip
  • 操作系统lab4-页面置换算法的模拟
  • 【excel】easy excel如何导出动态列
  • 设计模式之装饰器模式(SSO单点登录功能扩展,增加拦截用户访问方法范围场景)
  • Vue 3 介绍及应用
  • 假期增设:福祉与负担并存,寻求生活经济平衡之道
  • python中数据科学与机器学习框架
  • device靶机详解
  • 【C++ 基础数学 】2121. 2615相同元素的间隔之和|1760
  • 音频3A——初步了解音频3A
  • 【Python语言初识(一)】
  • [vulnhub] Hackademic.RTB1
  • 信息安全工程师(11)网络信息安全科技信息获取
  • 前端vue-作用域插槽的传值,子传父,父用obj对象接收
  • 服务设计原则介绍
  • html+css(交河故城css)
  • Python基于flask框架的智能停车场车位系统 数据可视化分析系统fyfc81
  • 【Windows 同时安装 MySQL5 和 MySQL8 - 详细图文教程】
  • Android15之源码分支qpr、dp、beta、r1含义(二百三十二)
  • 深度学习01-概述
  • JS 特殊运算符有哪些?
  • YOLOv8——测量高速公路上汽车的速度
  • Python一分钟:装饰器
  • 【Linux探索学习】第一弹——Linux的基本指令(上)——开启Linux学习第一篇
  • SpringCloud微服务消息驱动的实践指南
  • 环境部署-环境变量