个人技术分享

堆排序还有冒泡排序都在前面的文章写过在这一章节就不写了 

一、插入排序

一个数跟一个有序数组比较,最终这个数放在数组中的合适位置

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 (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else {
				break;
			}
		}
		a[end + 1] = tmp;
	}
}	

二、希尔排序(思路)

2.1希尔排序的推导

预排序:分组插入排序 

目标:大的更快换到后面,小的数更快换到前面的位置

思路:

假设间距 gap = 3

1.先逐个比较,比较的区间是[0, end]

 先看红色

1.1如果前面的数据比后面的数据大,后面的数据就要覆盖住

当end跳出循环时,a[end+gap] = tmp 

	int gap = 3;
	int end;
	int tmp = a[end + gap];
	//end大于0保持条件成立
	while (end >= 0)
	{
		if (tmp < a[end])
		{
			a[end + gap] = a[end];
			//循环一次end往前移动gap位置
			end -= gap;
		}
		else {//当tmp>gap时候跳出循环
			break;
		}
	}
	a[end + gap] = tmp;

单个数据比较后 

 2.进行这一趟的循环比较

当end为5的下标时候要停止循环

	int gap = 3;
	for (int i = 0; i < n - gap; i += gap)
	{
		int end = i;
		int tmp = a[end + gap];
		//end大于0保持条件成立
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + gap] = a[end];
				//循环一次end往前移动gap位置
				end -= gap;
			}
			else {//当tmp>gap时候跳出循环
				break;
			}
		}
		a[end + gap] = tmp;
	}

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];
			//end大于0保持条件成立
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					//循环一次end往前移动gap位置
					end -= gap;
				}
				else {//当tmp>gap时候跳出循环
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

不同的代码代表着不同的思路---以上的思路是红色的排完绿色的排然后紫色排

下面的代码优化表示着,gap组数据,交替完成分组插入排序

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];
			//end大于0保持条件成立
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					//循环一次end往前移动gap位置
					end -= gap;
				}
				else {//当tmp>gap时候跳出循环
					break;
				}
			}
			a[end + gap] = tmp;
		}
}

 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。此外希尔排序的时间复杂度大概是O(N^1.3) 

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap>1)
	{
        //无论gap为奇数还是偶数时,一直除到最后gap=1时就是插入排序
		gap /= 2;
        gap = gap/3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			//end大于0保持条件成立
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					//循环一次end往前移动gap位置
					end -= gap;
				}
				else {//当tmp>gap时候跳出循环
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

输出结果 

三、选择排序(调试)

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

下面代码进行优化---一次从待排序的数据元素中选出最大和最小的元素

 看下面的代码,看着思路很正确其实不对,接下来学习->如何快速找到问题

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		//最小,最大数的坐标
		int minI = begin;
		int maxI = end;
		//每遍历一遍,找到最小与最大
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[minI])
			{
				minI = i;
			}
			if (a[i] > a[maxI])
			{
				maxI = i;
			}
		}
		Swap(&a[begin], &a[minI]);
		Swap(&a[end], &a[maxI]);
		begin++;
		end--;
	}
}

调试除了F10,F11会很麻烦

上面可以直接打印每一趟的数组,直接发现问题 

直接按绿色箭头可以跳过循环,直接进入发现问题的上一级,之后进行逐步调试

直接跳过前3个循环,直接找到问题的源头

发现了一个问题--begin的坐标也为maxI的坐标,begin和minI交换后,maxI的坐标就发生了改变.

minI的坐标就变成了maxI的坐标

解决措施

 如果begin == maxI就让maxI = minI

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		//最小,最大数的坐标
		int minI = begin;
		int maxI = end;
		//每遍历一遍,找到最小与最大
		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[minI])
			{
				minI = i;
			}
			if (a[i] > a[maxI])
			{
				maxI = i;
			}
		}
		Swap(&a[begin], &a[minI]);
		if (begin == maxI)
		{
			maxI = minI;
		}
		Swap(&a[end], &a[maxI]);
		begin++;
		end--;
		print(a, n);
	}
}

四、快速排序 

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。 

4.1思路一 (递归)

单趟排序

找比key大和小的位置 

找到后就发生交换,一直到相遇 

 

相遇后,相遇的位置与key的位置发生交换---->为啥交换

 目的:key放到了最终位置------左边比key小右边比key大

为啥相遇位置比key要小?右边先走保证的

有两种情况:

L遇R:R先走,R在比key小的位置停下来了,L没有找到比key大的,就会跟R相遇,相遇位置R停下的位置,是比key小的位置

R遇L:第一轮R如果找不到小,R和L相遇在key

            第一轮先交换,此时L的位置小于key,R的位置大于key,R找小,没有找到,跟L相遇,此时相遇的位置就是L比key小

情景一、L遇R 

情景二、R遇L

 

之后利用二叉树的思想

以下是单趟排序的常规问题------接下来要找特殊问题(之后所有的题目都要先找常规问题在找特殊问题) 

void PartSort(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		//right先走找小---->右边先走才能保证相遇位置比key小
		while (a[right] > a[key])
		{
			right--;
		}
		//left再走,找大
		while (a[left] < a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[key]);
}

情景一,右边全是比key大的 

 

此时要修改while(a[left] < a[key])里面的条件要保障key<right 

 情景二, 与key下标相等的值

就会进行死循环

两种情况考虑好后,修改代码----->单趟排序无问题

void PartSort(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		//right先走找小
		while (left < right && a[right] >= a[key])
		{
			right--;
		}
		//left再走,找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[key]);
}

单趟排序结束后进行多趟排序

多趟排序

[ ]key[ ]    left和right位置发生改变,只能提前保留一份 用begin和end保留

[begin  key-1] key [key+1  end]

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	int key = left;
	while (left < right)
	{
		//right先走找小
		while (left < right && a[right] >= a[key])
		{
			right--;
		}
		//left再走,找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[key]);
	key = left;
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

时间复杂度是O(N*logN) 每层遍历要进行N层,高度是logN

当有序的情况下,快速排序时间复杂度最坏是O(N^2)

每一趟要遍历N次高度从logN变成了N

导致时间复杂度变成了O(N^2) 

接下来进行优化

4.1.1随机选key

1.选择一个随机下标

2.下标与left的值进行交换

void QuickSort(int* a, int left, int right)
{
	// 区间只有一个值或者不存在就是最小子问题
	if (left >= right)
		return;
	int begin = left, end = right;


	//选区间中的随机数做key
	int randi = rand() % (right - left + 1);
	思考为啥加left
	当进行子区间的时候加上left才是随机数的下标
	randi += left;
	Swap(&a[left], &a[randi]);

	int keyi = left;
	while (left < right)
	{
		// right先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		// left再走,找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

4.1.2 三数取中

1.left   mid   right

2.以上三个下标进行比较,谁是中间值就为key

//三数取中  left mid right
int GetMidi(int* a ,int left,int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[right])
	{
		if (a[right] > a[mid])
		{
			return right;
		}//a[right]<a[mid]
		else if (a[mid] > a[left])
		{
			return left;
		}
		//a[right]<a[mid]
		//a[mid]<a[left]
		else {
			return mid;
		}
	}//a[left]<a[right]
	else
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		//a[left]<a[right]
		//a[mid]>a[left]
		else if (a[right] < a[mid])
		{
			return right;
		}
		//a[left]<a[right]
		//a[mid]>a[left]
		//a[right]>a[mid]
		else {
			return mid;
		}
	}
}

以上只是对有序数组进行了优化,但是数组元素全部相同,时间复杂度依旧是O(N^2) 

4.1.3小区间优化 

递归建立栈帧也是有消耗的---->尤其是最后次数的递归占的比例太大

 

最后一层递归占比50%最后第二层递归占比25%

 为了减少递归的次数---->最后几层采用插入排序以来减少递归的次数

//小区间选择走插入,可以减少90%的递归
if (right - left + 1 < 10)
{
	InsertSort(a + left, right - left + 1);
}

思考为什么传递的是(a + left )

和随机选key+left思路一样

4.2思路二 (递归)

4.2.1前后指针

非常非常简单的方法:推箱子

prev和cur关系

1.prev 紧跟 cur 一前一后

2.prev和cur之间是比key大的值  

void QuickSort2(int* a,int left,int right)
{
	if (left >= right)
		return;
	int keyi = left;
	int prev = left;
	int cur = left + 1;

	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			++prev;
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	//单趟数据交换完以后直接换值然后换下标
	Swap(&a[keyi], &a[prev]);
	keyi = prev;

	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort2(a, left, keyi - 1);
	QuickSort2(a, keyi + 1, right);
}

4.3思路三(非递归)

用数据结构中的栈(重要

栈里面存区间

void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);

	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);

		int end = STTop(&st);
		STPop(&st);

		// 单趟
		int keyi = begin;
		int prev = begin;
		int cur = begin + 1;

		while (cur <= end)
		{
			if (a[cur] < a[keyi] && ++prev != cur)
				Swap(&a[prev], &a[cur]);

			++cur;
		}

		Swap(&a[keyi], &a[prev]);
		keyi = prev;

		// [begin, keyi-1] keyi [keyi+1, end] 
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}

		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}

	STDestroy(&st);
}

五、 归并排序

  归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

5.1归并(递归) 

 步骤一、分治

采用的是后序,先计算左子树再计算再计算右子树

步骤二、归并

先,进行比较,得出有序数先给临时数组tmp

之后,进行数组的保存

把tmp中的有序数组赋值给a中,方便下次比较。

void _MergeSort(int* a, int begin,int end,int*tmp)
{
	if (begin == end)
	{
		return;
	}
	//分治
	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	//归并
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	//插入
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else {
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a+begin, tmp+begin,sizeof(int)*(end-begin+1));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

5.2归并(非递归 )

直接改循环 

 

如果e1,b2超出范围那就停止归并

但是e2超出范围就要修改e2的值

void MergeSortNonR(int* a,int begin, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap<n)
	{
		for (int j = begin; j < n; j += 2 * gap)
		{
			int begin1 = j, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;

            //超出范围作出改变
			if (begin2 > n || end1 > n)
				break;
			if (end2 > n)
				end2 = n - 1;


			//插入
			int i = j;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[i++] = a[begin1++];
				}
				else {
					tmp[i++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[i++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = a[begin2++];
			}
			memcpy(a+j, tmp+j, sizeof(int) * (end2 - j + 1));
		}
		gap = gap * 2;
	}
	
	return 0;
}

六、非比较排序 

6.1计算排序

计算排序就好比是投票计数时写正字

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(MAX(N,Range))
3. 空间复杂度:O(Range)
4. 稳定性:稳定

什么是稳定--->稳定好比   2  2  3  4  5  1  3 

 1  2  2  3  3  4  5   原来相等的两个元素前后相对位置在排序后依然不变

void CountSort(int* a, int n)
{
    //找最大最小求范围
	int min = a[0], max = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];

		if (a[i] < min)
			min = a[i];
	}
	int range = max - min + 1;

	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
    //全部制为0
	memset(count, 0, sizeof(int) * range);

	// 统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	// 排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}