数据结构——排序

目录

插入排序

希尔排序 

选择排序 

交换排序 

冒泡排序 

快速排序 

hoare版本 

 挖坑法

 前后指针法

 非递归


给大家推荐一款刷题,找工作的好网站——牛客网

牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网

 

插入排序

 思路(这里以升序为例):

1.先用一个变量接收最后一个元素,这里是tmp

2.用tmp保存的元素与前面的进行比较,如果比前面小则把前面的覆盖到end+1的位置

3.覆盖之后end继续向前走,end+1,自然也跟着向前走,此时可以看到tmp的作用,就是保存最后一个元素

4.当end<0时,一轮比较结束,此时将tmp的值传给end+1,即队首的位置,还有一种情况可使一轮比较结束

当end位置的元素<tmp时,一轮比较结束,进行下一轮比较

void InsertSort(int* a, int n)
{
	int end = n - 1;
	int tmp = a[end + 1];
	for (int i = 0; i < n-1; i++)
	{
		end = i;
		tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}

}
int main()
{
	int arr[] = {9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	InsertSort(arr, n);
	return 0;
}

 直接排序时间复杂度:O(N^2)

希尔排序 

1.预排序:接近有序,将间隔为gap的数据分成一组

设置一个gap将数组分为几组,这里gap是3,将数组分为了3组

9 5 8 5一组,1 7 6一组,2 4 3 一组

 

我们先将第一个红色部分的进行排序,我们可以看到红色部分是按照升序的

void ShellSort(int* a, int n)
{
	int gap = 3;
	int end = n - 1;
	int tmp = a[end + gap];
	for (int i = 0; i < n-gap; i+=gap)
	{
		end = i;
		tmp = a[end + gap];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end=end-gap;
			}
			else
				break;
		}
		a[end + gap] = tmp;
	}

}
int main()
{
	int arr[] = {9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	InsertSort(arr, n);
	return 0;
}

 2.对3个组分别进行排序,使这三个部分有序

void ShellSort(int* a, int n)
{
	int gap = 3;
	for (int j = 0; j < gap; ++j)
	{
		for (int i = j; i < n - gap; i += gap)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}
int main()
{
	int arr[] = {9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	ShellSort(arr, n);
	return 0;
}

也可以这样

void ShellSort(int* a, int n)
{
	int gap = 3;
		for (int i = 0; i < n - gap; i ++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
}
int main()
{
	int arr[] = {9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	ShellSort(arr, n);
	return 0;
}

3.对整体进行排序 

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//或gap=gap/2;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}
int main()
{
	int arr[] = {9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	ShellSort(arr, n);
	return 0;
}

当gap是1的时候是直接排序,其余情况是预排序

希尔排序时间复杂度:O(N^1.3)

选择排序 

// 最坏时间复杂度:O(N^2)
// 最好时间复杂度:O(N^2)
void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		// 选出最小的放begin位置
		// 选出最大的放end位置
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; ++i)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}

			if (a[i] < a[mini])
			{
				mini = i;
			}
		}

		Swap(&a[begin], &a[mini]);
		// 修正一下maxi
		if (maxi == begin)       //如果没有这条语句,当最大数是首元素时,前面语句会把最小的元素换到首元素,而此时maxi指向首元素,首元素却不是最大值,后面再交换就会出错
			maxi = mini;

		Swap(&a[end], &a[maxi]);
		++begin;
		--end;
	}
}

 1.一开始让maxi 和 mini 为数组首元素的下标

2. 接着遍历数组,进行比较替换

直接选择排序最慢的排序之一

选择排序的时间复杂度不像前面几种排序方法那样,前面几种排序方法的时间复杂度不是一眼就能看出来的,而是要通过推导计算才能得到的。一般会涉及到递归和完全二叉树,所以推导也不是那么容易。但是选择排序就不一样了,你可以很直观的看出选择排序的时间复杂度:就是两个循环消耗的时间;
       比较时间:T = (n-1))+ (n -2)+(n - 3).... + 1;  ===>>  T =  [n*(n-1) ] / 2;
      交换时间:最好的情况全部元素已经有序,则 交换次数为0;最差的情况,全部元素逆序,就要交换 n-1 次;
       所以最优的时间复杂度  和最差的时间复杂度   和平均时间复杂度  都为 :O(n^2)

2.使用选择排序对长度为100的数组进行排序,则比较的次数为( )

A.5050

B.4950

C.4851

D.2475

答案:B

解析:

选择排序,每次都要在未排序的所有元素中找到最值,

如果有n个元素,则

第一次比较次数: n - 1

第二次比较次数: n - 2

....

第n - 1次比较次数: 1

所有如果n = 100

则比较次数的总和:99 + 98 + ...... + 1

共4950次。

交换排序 

冒泡排序 

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

 

比较集中排序,这里十万个数据,冒泡排序很慢

冒泡排序:最坏情况时间复杂度:O(N^2),最好情况O(N)

快速排序 

hoare版本 

1.设置最左边(或最右边)一个值为key,把比key小的key的放在key左边,大于key的放在key右边,这里key是6

 2.如何保证相遇位置比key要小,让R先走即可,这种情况是让左边第一个做key,同理右边第一个做key,让L先走,即可在小于K处相遇

3.相遇之后交换相遇位置的数和key所指的数即可


int PartSort(int* arr, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		
		if (left<right)
			Swap(&arr[left], &arr[right]);
	}
	int meeti = left;
	Swap(&arr[meeti], &arr[keyi]);
	return meeti;
}

单趟排序

 要对所有数据排序,进行递归即可

如何递归?

将keyi左边先递归排序,然后再右边


int PartSort(int* arr, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		
		if (left<right)
			Swap(&arr[left], &arr[right]);
	}
	int meeti = left;
	Swap(&arr[meeti], &arr[keyi]);
	return meeti;
}
//部分排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return; //如果左指针>=右指针,返回
	}
	int keyi=PartSort(a, begin, end);
	QuickSort(a, begin, keyi - 1);//
	QuickSort(a, keyi+1, end);
}
//快排
int main()
{
	int arr[] = {9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort(arr, 0, n - 1);
	return 0;
}

当每次选key为中间值时,递归深度:logN,每次单趟排序合计起来个数是N,时间复杂度:O(N*logN)。 

快排比堆排,希尔排序略胜一筹 

单趟排序,如果选中间做key,如果还有一个大小为key的值在key的左边或右边,就不好搞。

如果有序/接近有序,选左边排序,比key大的在左边,没有比key小的,key的左边为空,之后递归右边

 

如果这样一直往下走,树的高度是N,每次递归都会少一个数

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

 debug,N=10000,就会栈溢出,此时换realse(100000个数据)版本看O(N^2)情况下的运算情况

100W个数据

 优化选key逻辑:1.随机选一个位置做key(如果每次选最左边或最右边有可能会发生跟上面O(N^2)一样的情况,但如果随机选位置就算有序,每次选的数不一定是最左边或最右边)

                             2.针对有序情况,选正中间的值做key,之后把key和最左边或最右边的交换即可

                            3.三数取中,第一个位置,中间位置,和最后一个位置,选出这三个位置上的中间数据值,然后再跟最左边数据交换

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int GetMindIndex(int* arr, int left, int right)
{
	int mid = (right + left) / 2;
	if (arr[left] < arr[mid])
	{
		if (arr[mid] < arr[right])
			return mid;
		else if (arr[right]<arr[left])
			return left;
		else
			return right;
	}
	else
	{
		if (arr[mid] > arr[right])
			return mid;
		else if (arr[right] > arr[left])
			return left;
		else
			return right;
	}
}
int PartSort(int* arr, int left, int right)
{
	int mid = GetMindIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		
		if (left<right)
			Swap(&arr[left], &arr[right]);
	}
	int meeti = left;
	Swap(&arr[meeti], &arr[keyi]);
	return meeti;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort(a, begin, end);
	QuickSort(a, begin, keyi - 1);//
	QuickSort(a, keyi + 1, end);
}
int main()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort(arr, 0, n - 1);
	return 0;
}

测试1000W个数据

跟上面O(N^2)的相比,提高了不少,不用害怕栈溢出,因为其深度不会太深 ,深度是logN

小区间优化

倒数三层合计占用80%栈帧 

这种形态有点像二叉树,最后一层占了一半的调用次数,每次调用就会开辟栈帧,如果倒数第二层有8个数进行排序,则要开辟三层栈帧, 8个值本身就是小范围,调用三层栈帧,调用7-8次递归,比较浪费栈帧空间。付出的代价太大

因此当数据<=8时,我们用插入排序,其余用快排

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int GetMindIndex(int* arr, int left, int right)
{
	int mid = (right + left) / 2;
	if (arr[left] < arr[mid])
	{
		if (arr[mid] < arr[right])
			return mid;
		else if (arr[right]<arr[left])
			return left;
		else
			return right;
	}
	else
	{
		if (arr[mid] > arr[right])
			return mid;
		else if (arr[right] > arr[left])
			return left;
		else
			return right;
	}
}
int PartSort(int* arr, int left, int right)
{
	int mid = GetMindIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		
		if (left<right)
			Swap(&arr[left], &arr[right]);
	}
	int meeti = left;
	Swap(&arr[meeti], &arr[keyi]);
	return meeti;
}
void InsertSort(int* a, int n)
{

		for (int i = 0; i < n -1; i++)
		{
			int end = i;
			int tmp = a[end +1];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end +1] = a[end];
					end --;
				}
				else
				{
					break;
				}
			}
			a[end +1] = tmp;
		}
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin <= 8)
	{
		InsertSort(a + begin, end - begin + 1); //a+begin是让从左边开始,每次递归都会导致左边不一样
	}
	else
	{
		int keyi = PartSort(a, begin, end);
		QuickSort(a, begin, keyi - 1);//
		QuickSort(a, keyi + 1, end);
	}

}
int main()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort(arr, 0, n - 1);
	return 0;
}

我们可看到这种写法有所优化 

 挖坑法

1. 先创建一个变量将坑位的值保留,建立俩个指针,分别指向最左边和最右边,若左边为坑,则右指针先走,反之,左指针先走

2.当右指针遇到比key小的数字,停下来,并把该数字放到hole所指位置 (即填到坑里),自己形成新的坑位

 

3.然后让左边走,遇到比key大的停下来,把数字填到坑里,然后自己相乘新坑位

void InsertSort(int* a, int n)
{

		for (int i = 0; i < n -1; i++)
		{
			int end = i;
			int tmp = a[end +1];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end +1] = a[end];
					end --;
				}
				else
				{
					break;
				}
			}
			a[end +1] = tmp;
		}
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin <= 8)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort2(a, begin, end);
		QuickSort(a, begin, keyi - 1);//
		QuickSort(a, keyi + 1, end);
	}

}
//挖坑法
int PartSort2(int* arr, int left, int right)
{
	int mid = GetMindIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int hole = left;
	int key = arr[left];
	while (left < right)
	{
		while (left < right && arr[right] >= key)
		{
			right--;
		}
		arr[hole]=arr[right];
		hole = right;
		while (left < right && arr[left]<= key)
		{
			left++;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	return hole;
}

 性能跟之前的差不多

 前后指针法

1.设置俩个指针和一个key,key用来保存数据,俩个指针一前一后,prev在后,cur在前

2.cur先走,prev接着走,当prev的后一个数字比key大时(也就是cur遇到比key大的数字),prev停下来,cur,继续走,cur遇到比key小的值停下来

3.停下来之后,prev向前走一步,交换俩数字,之后按照这种方法继续走就行,当cur走到数组之外时,停止,prev会在小于key的位置停下

int PartSort3(int* arr, int left, int right)
{
	int mid = GetMindIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;
	int cur = left + 1;
	int prev = left;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)//等于可以不用动那个值,让他留在哪里
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	Swap(&arr[keyi], &arr[prev]);
	return prev;
}
int main()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort(arr, 0, n - 1);
	return 0;
}

 非递归

使用递归方式进行快排时,若递归层数较多,则会造成栈溢出,我们可以用数据结构的“栈”来用非递归实现 

先把起点和终点的下标放进去,这样相当于把第一行的,然后用left和right变量来接收这俩个值,并把这俩个值出栈left=0,right=9

 出栈后,进行排序,并返回他们中间数据的下标,由于我们递归的时候是先递归左边,再递归右边,所以用栈实现的时候也是先对左边排序,再对右边排序,但由于栈是先入后出,所以要先把右边压栈,再把左边压栈,这样出去的时候就是左先出,先计算左,然后右出,再计算右,压栈的时候左和右要和程序里面的left和right相对应

 我们这里在设计的时候是先给右赋值,后给左赋值,再结合我们先给左半边排序再给右半边排序的规则,我们先压5,再压9,之后先压0,再压3

当中间值下标+1>=右边或者左边+1>=中间值下标的时候我们不需要压栈

比如这里最后一行最左边0 中间值下标 0 最右边 0,如果进行压栈待会出栈会出来2个0,而数组里面只有一个0,会影响程序的正确性

int PartSort3(int* arr, int left, int right)
{
	int mid = GetMindIndex(arr, left, right);
	Swap(&arr[mid], &arr[left]);
	int keyi = left;
	int cur = left + 1;
	int prev = left;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)//等于可以不用动那个值,让他留在哪里
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	Swap(&arr[keyi], &arr[prev]);
	return prev;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin <= 8)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);
		QuickSort(a, begin, keyi - 1);//
		QuickSort(a, keyi + 1, end);
	}

}

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);

		int left = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort3(a, left, right);
		// [left, keyi-1] keyi [keyi+1,right]

		if (keyi + 1 < right)//限制条件
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}

		if (left < right - 1) //限制条件
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
	}

	StackDestory(&st);
}


int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSortNonR(arr, 0, n - 1);
	return 0;
}

评论 53
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头发没有代码多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值