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

news2024/11/19 7:31:37

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

目录

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.coloradmin.cn/o/2162016.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PyTorch模型转ONNX量化模型

你是否发现模型太大&#xff0c;无法部署在你想要的云服务上&#xff1f;或者你是否发现 TensorFlow 和 PyTorch 等框架对于你的云服务来说太臃肿了&#xff1f;ONNX Runtime 可能是你的救星。 如果你的模型在 PyTorch 中&#xff0c;你可以轻松地在 Python 中将其转换为 ONNX…

关于YOLOX的一些优势

YOLOX 是旷视开源的高性能检测器。旷视的研究者将解耦头、数据增强、无锚点以及标签分类等目 标检测领域的优秀进展与 YOLO 进行了巧妙的集成组合&#xff0c;提出了 YOLOX&#xff0c;不仅实现了超越 YOLOv3、 YOLOv4 和 YOLOv5 的 AP&#xff0c;而且取得了极具竞争力的推理速…

FME学习笔记

读取数据 方法一&#xff1a;add reader 通过读模块来进行数据的读取 方法二&#xff1a;FeatureReader Parameters 通过转换器来进行数据的读取 可以通过空间范围进行筛选 在FME中&#xff0c;所有数据处理都要用到的&#xff0c;绝对的重点&#xff1a;转换器&#xff…

深圳某局联想SR850服务器黄灯 不开机维修

深圳 福田区1台Lenovo Thinksystem SR850 四路服务器黄灯问题现场处理。 1&#xff1a;型号&#xff1a;联想SR850 机架式2U服务器 2&#xff1a;故障&#xff1a;能通电&#xff0c;开机按钮快闪&#xff0c;随后叹号警告灯常亮 3&#xff1a;用户自行折腾无果后找到我们tech …

【推文制作】秀米简明教程 1.0

【前言】本文内容主要是针对一些常用的秀米操作进行介绍&#xff0c;并说明一些往年的经验要求。但是&#xff0c;最重要的是&#xff0c;请发挥你的艺术创造力&#xff0c;相信你一定可以做出更好看的推文。 一、秀米页面介绍 在使用秀米之前&#xff0c;我们会有一个通过浏览…

Maya学习笔记:项目设置和快捷键

文章目录 项目设置工程文件夹 快捷键 项目设置 工程文件夹 maya需要一个文件夹存放自己的工程内容 先指定一个文件夹 文件/项目窗口 选择一个文件夹&#xff0c;然后选择创建默认工作区 然后生成文件目录 在项目窗口里&#xff0c;选择要生成的子文件夹&#xff08;保持默认…

【ASE】第二课_溶解效果

今天我们一起来学习ASE插件&#xff0c;希望各位点个关注&#xff0c;一起跟随我的步伐 今天我们来学习溶解效果&#xff0c;通过渐变纹理达到好像燃烧效果的溶解效果 今天我们的效果很简单&#xff0c;但是其中包含没有学习的节点&#xff0c;所以还是要拿出来学习一下 最终…

ESP32异常报错2

出现这种情况 一般是缺少";"分号. 或者缺少, 仔细查找代码.查看是哪儿缺少了这些代码

【2024W35】肖恩技术周刊(第 13 期):肉,好次!

周刊内容: 对一周内阅读的资讯或技术内容精品&#xff08;个人向&#xff09;进行总结&#xff0c;分类大致包含“业界资讯”、“技术博客”、“开源项目”和“工具分享”等。为减少阅读负担提高记忆留存率&#xff0c;每类下内容数一般不超过3条。 更新时间: 星期天 历史收录:…

docker快速部署zabbix

两台主机 一台作为server 一台作为agent 安装好docker 并保证服务正常运行&#xff0c;镜像正常pull 分析&#xff1a; 部署 Zabbix 容器环境&#xff0c;通常会涉及几个主要组件&#xff1a; MySQL&#xff08;或 MariaDB 数据库&#xff09;、Zabbix Server 和 Zabbix Web I…

c++ 继承 和 组合

目录 一. 继承 1.1 继承的概念 1.2 继承定义 1.3 继承类模板 1.4. 继承中的作用域 二. 派生类&#xff08;子类&#xff09;的默认成员函数 2.1 概念&#xff1a; 2.2 实现⼀个不能被继承的类 2.3 继承与友元 2.4继承与静态成员 三.多继承及其菱形继承问题 3.1继承方…

物联网实践教程:微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——汇总

物联网实践教程&#xff1a;微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——汇总 前言 之前在学校获得了一个新玩意&#xff1a;ESP-01sWIFI模块&#xff0c;去搜了一下这个小东西很有玩点&#xff0c;远程控制LED啥的&#xff0c;然后我就想…

CUDA编程三、C++和cuda实现矩阵乘法SGEMM

目录 一、矩阵SGEMM 二、SGEMM的各种实现 1、cpu版本的实现 2、GPU并行计算最初始的版本 GPU中数据的移动 3、矩阵分块Shared Memory优化 4、LDS.128 float4* 优化 5、__syncthreads()位置优化 6、blank conflict优化 bank概念 bank conflict bank conflict危害和处…

IO其他流

1. 缓冲流 昨天学习了基本的一些流&#xff0c;作为IO流的入门&#xff0c;今天我们要见识一些更强大的流。比如能够高效读写的缓冲流&#xff0c;能够转换编码的转换流&#xff0c;能够持久化存储对象的序列化流等等。这些功能更为强大的流&#xff0c;都是在基本的流对象基础…

yum库 docker的小白安装教程(附部分问题及其解决方案)

yum库 首先我们安装yum 首先在控制台执行下列语句 首先切换到root用户&#xff0c;假如已经是了就不用打下面的语句 su root #使用国内的镜像&#xff0c;不执行直接安装yum是国外的&#xff0c;那个有问题 curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.al…

大模型框架 LangChain 介绍

文章目录 langchain介绍安装依赖大模型类别千帆大模型案例常见问题 langchain介绍 是一个开源大语言模型框架&#xff0c;本身不提供大模型算法&#xff0c;只提供对接大模型算法平台的接口&#xff08;模型包裹器&#xff09;&#xff1b;langchain官网v0.2&#xff0c;内部涉…

POI获取模板文件,替换数据横纵动态表格、折线图、饼状图、折线饼状组合图

先说几个关键的点 pom.xml依赖 <dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version> </dependency> <dependency><groupId>com.deepoove</groupId>&…

现代桌面UI框架科普及WPF入门1

现代桌面UI框架科普及WPF入门 文章目录 现代桌面UI框架科普及WPF入门桌面应用程序框架介绍过时的UI框架MFC (Microsoft Foundation Class)缺点 经典的UI框架**WinForms****QT****WPF** 未来的UI框架**MAUI****AvaloniaUI** WPF相对于Winform&#xff0c;QT&#xff0c;MFC的独立…

【深度学习】(5)--搭建卷积神经网络

文章目录 搭建卷积神经网络一、数据预处理1. 下载数据集2. 创建DataLoader&#xff08;数据加载器&#xff09; 二、搭建神经网络三、训练数据四、优化模型 总结 搭建卷积神经网络 一、数据预处理 1. 下载数据集 在PyTorch中&#xff0c;有许多封装了很多与图像相关的模型、…

二阶滤波算法总结(对RC滤波算法整理的部分修正和完善)

文章目录 1、一阶低通滤波2、一阶高通滤波3、二阶低通滤波器3.1 二阶RC低通滤波器的连续域数学模型3.2 二阶RC低通滤波器的算法推导3.3 matlab仿真 4、二阶高通滤波器4.1 二阶RC高通滤波器的连续域数学模型4.2 二阶RC高通滤波器的算法推导4.3 matlab仿真 5、陷波滤波6、带通滤波…