数据结构-关于常用排序方法的理解

一、排序的概念与理解

日常生活中,我们有很多东西都是会被进行排序组合的,比如说你在一个网购网站买东西,你可以选择价格优先排序,从低到高或者从高到低的方式,亦或者是销售量优先,新品优先,都是一种排序。排序,是将我们的需要检索的东西按照我们想要的方式进行的排列整合的体现。将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。其目的是将一组“无序”的记录序列调整为“有序”的记录序列。

比如下方的网购图片排序方式

二、常用排序方法的概念与实践

我们下面会学习到的排序方法基本是比较类排序,什么意思呢,就是说,把你要比较的所有的值进行比较大小,然后进行升序或者降序排序,比方说1234567,你如果要降序,排完就是7654321,下面我们直接进入正题。

1.插入排序

比方说我们现在有三个数,然后我们要对他们进行排序,我这里包括后面一般都会用升序,在这里先说一下。那我们可以怎么排呢?我这里用的是创建一个临时变量,存储第二个值,然后让第一个值和第二个值作比较,如果大与第二个值,则把第一个值赋给第二个值的位置,再把我们刚刚保存的值的那个变量,赋给第一个值,这里我们就实现了交换。

有了第一趟,那么,我们比较第三个的时候,我们在这里加一个循环,让它比较完前两个,再比较第二个和第三个,就实现基于原数组的比较排序。

这里end是0的时候,会比较第一个和第二个值,当end是1的时候就会比较第二个和第三个值,然后--end后,会变成0,这里会再次比较第一个和第二个值。比方说7,4.这里排一趟就是4,7.此时end是0.如果是5,6,4.这里排第一趟是5,4,6.然后end--,变成0,再排一趟就是4,5,6.

void InsertSort(int* arr, int n)
{

	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)//这里每次都会再次排序,0排一次,1排两次
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else
			{
				break;
			}
			arr[end + 1] = tmp;
		}
		
	}
	
}
2.希尔排序

​希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因D.L.Shell于1959年提出而得名。

希尔排序分为两步,第一步是将要排序的数组进行分组,分组的组数量原则上小于要排序的值的总数,因为总共就那么多数,最多也就排等于排序的值的总数的组,也就是1,这里其实就相当于直接插入排序了。

然后第一步排好了,每组进行排序好了之后,就进行最后一步排序,也就是当分组数等于排序总数的时候。

这里我们将要分组的名字取为gap,gap越大,大的数会更快到后面,gap越小,数据跳动越慢,就越接近有序,直到最后一次gap等于1时,实现的功能就相当于插入排序,换言之,希尔排序是直接插入排序的优化。

这里取数:9,1,2,5,7,4,8,6,3,5

这里的话,比如说gap是3,就是我们分了三组,假设我们这里取   数:9,1,2,5,7,4,8,6,3,5  那么第一组就是9,5,8,5,第二组是1,7,6.第三组是2,4,3.因为我们这里是10个数嘛,然后分了三组,第二次的话进入循环再除3,就是1,就直接插入排序了。

gap是2的时候也是一样的,只是分的组的数量不一样而已。

void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//只要大于1都是预排序,等于1就相当于插入排序,最后一步很重要,即使前面的预排序没有排很好,最后一下也会排好
		gap = gap / 2;
		/*gap = gap / 3 +1;*/
		for (int i = 0; i < n - gap ; i ++)//一步排序
		{
			int end = i;
			while (end >= 0)
			{
				int tmp = arr[end + gap];
				if (arr[end] > arr[end + gap])
				{
					arr[end + gap] = arr[end];
					--end;
				}
				else
				{
					break;
				}
				arr[end + 1] = tmp;
			}
		}
	}	
}

 

 打印一下看看怎么样

是可以的,希尔排序能运用的场景很多,首先它的这个预排序,可以让很多时候都节省一定的空间去达到相同的效果,而且理解起来也不是很难,不过如果是很多个值相等的情况下,希尔排序不能保证这些值的位置不会发生改变,所以希尔排序又是不稳定的算法之一。

3.选择排序

这个的话比较简单粗暴,就是一个一个值去找。

先从待排序的数据元素中选出最大(或最小)的一个元素,存放在序列的起始位置,再选出次大的放到第二个位置,依次循环往复,直到全部待排序的数据元素排完 。

直接插入排序的思想呢非常简单,但是它的效率比较低,每遍历一次才选出一个数。

那么,我这里给大家说一种比较快同时也粗暴的方法。

 两两选择法

一趟排序中,选出最小值和最大值,让它们保持在最左边和最右边,然后缩小这个区间,找出次大和次小的值,再次查找,再次选择,这样的话就能保证左边小右边大的排序了。

看下运行结果

void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	/*int max = begin;
	int min = begin;*/
	//将 max 和 min 的定义放在 while 循环外部
	//这种情况下,max 和 min 的作用域跨越了整个 while 循环
	//它们的值在每次循环迭代时会被保留和更新     
	//将 max 和 min 的定义放在 while 循环内部
	//这种情况下,max 和 min 的作用域仅限于每次 while 循环迭代
	//当循环迭代结束时,它们的值会被销毁,下一次循环重新定义和初始化
	while (begin < end)
	{
		int max = begin;
		int min = begin;
		//在选择排序算法中,正确的做法是将 max 和 min 的定义放在 while 循环内部,以确保它们在每次循环迭代时都能被正确初始化和使用
		for (int i = begin + 1; i <= end; i++)
		{//第一趟begin是0,max,min也是0
			if (arr[i] > arr[max])
			{
				max = i;
			}
			if (arr[i] < arr[min])
			{
				min = i;
			}
		}
		Swap(&arr[min], &arr[begin]);
		if (max == begin)
		{
			max = min;
		}
		Swap(&arr[max], &arr[end]);
		begin++;
		end--;
	}
}

 代码里面有时会记录我发生的一些错误的一些心得,有些错不一定后面不会再犯,只能说接触的多了就会越来越小心,大家看到权当图一乐吧哈哈哈。

4.堆排序(之前二叉树有讲过)

堆的话主要是二叉树的思想,首先生出一个根节点,这个节点也是所有节点的父节点,然后再生出左子节点,右子节点.......

节点这里的话我取得是第一个值,画图的话,这样的看着比较好理解一些,然后让父节点和子节点进行比较,父节点大了就交换,因为我们是升序排列。

测试一下看看有没有问题,之前二叉树的文章也可以看看,写的不好还请指正!

void HeapDown(int* a, int n, int parent)//大堆,越来越xiao
{
int child = parent * 2 + 1;
while (child < n)
{
	if (a[child] < a[child + 1] && child + 1 < n)  //第一个箭头一换,就变逆序
	{
		child++;
	}
	if (a[parent] < a[child])//箭头一换,就变逆序
	{
		Swap(&a[child], &a[parent]);
		parent = child;//
		child = parent * 2 + 1;//
		/*HeapDown();*/ //向下排序
		/*HeapUp(); */ //向上排序
	}
	else
	{
		break;
	}
}
}
void HeapSort(int* a, int size)
{
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)
	{
		HeapDown(a, size, i);

		//这一步其实已经实现了降序排序
	}
	int count = size - 1;
	while (count > 0)
	{
		//加上这一步就是升序
		Swap(&a[0], &a[count]);
		HeapDown(a, count, 0);
		count--;
	}
}

堆排序使用堆来选数,效率就高了很多。
时间复杂度:O(N*log2N)
空间复杂度:O(1)
稳定性:不稳定
举个例子吧。
比如这样一组数据:9 9 9 9 9 9
建好堆之后最后一个元素和堆顶一交换,是不是就不行了。
当然有些数据可能在向下调整建堆的过程中就不稳定了。

5.快速排序(多版本)

快速排序也叫快排,快速排序的基本思想: 通过一趟排序将待排序的序列分割为左右两个子序列,左边的子序列中所有数据都比右边子序列中的数据小,然后对左右两个子序列继续进行排序,直到整个序列有序。

这里的多版本包括hoare霍尔排序,挖坑法,前后指针法,还有一个通过栈模拟的非递归的快排,以及关于递归版本的优化方案,写的可能有点乱,因为很多我是分开写的方法,然后调用而已。

hoare霍尔排序

这里的话是把第一个值设为key,然后左边找大,右边找小,左边的值没有key大,就++,右边的值没有key小,就--。两个都找到了的时候,就交换它们的值,一直到它两相遇,也就是左边都是小的,右边都是大的了,再把相遇位置的值跟key换,就实现了单趟排序。

然后的话,我们只需要通过多次的调用这个排序,就可以实现霍尔大佬的思想了。

int HoareSort(int* arr, int left, int right)//0,n
{
	//三数取中优化
	int mid = GetMidIndex(arr, left, right + 1);
	Swap(&arr[left], &arr[mid]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])
		{
			right--;
		}
		//这里有点惭愧,我一直在调用keyi,实际是arr[keyi],然后找了很久也没发现问题所在,细节还是很重要的
		while (left < right && arr[left] <= arr[keyi])//这里应该是循环不是判断,一直找到比它大才停止,不是只判断一次
		{
			left++;
		}

		if (left < right)
		{
			Swap(&arr[left], &arr[right]);
		}
		//++left;
		//--right;
	}
	int nextkey = left;
	Swap(&arr[nextkey], &arr[keyi]);
	return nextkey;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if( end-begin == 10)
	{
		InsertSort(arr, end +1 );//因为传过来是下标的最大值,所以这里加一才是数组长度
		return arr;
    }
	int key = HoareSort( arr, begin, end);

	/*int key = PointerSort(arr, begin, end);*/
	QuickSort(arr, begin, key - 1);
	//这里有点惭愧,我一直在调用keysort,然后找了很久也没发现问题所在,细节还是很重要的
	QuickSort(arr, key + 1, end);

}

 那个mid是三数取中,我这里也讲一下,算是一个小的优化。

首先我们选key是选最左边的值,那么如果我们的key是最大值的时候,我们又要升序,就会造成right一趟走完都没有比key小的数,然后key就会和left也就是第一个值和第二个值交换,如果是刚好整个数组都是逆序排好需要我们升序的时候,那么就相当于每一次都需要我们去交换,此时复杂度就是n^2了,三数取中就是优化这种情况或者说这类情况诞生的。

我们选最左,最右和中间三个值,然后比较,选择不是最大也不是最小的那个值作为我们的key,就可以让复杂度不是n^2了,当然有很多相近数或者相同数的话,这种情况下复杂度还是n^2。

还有一个优化就是小区间优化,就是要排序的数组范围相对来说不大的话,就用插入排序,减少频繁调用递归的次数。

挖坑法

挖坑法的思想其实和hoare排序的思想差不多,先把我们的key保存到一个临时变量中,然后定义两个变量,左边找大,右边找小,右边找到了就塞到key的位置,左边找到了就塞到右边空出来的坑,到最后一个坑就把保存的key塞进去。

这里感觉的话也算是hoare的优化,不用注意太多,跑起来就行,怎么说来着,程序和人有一个能跑就行,欸嘿~  

int HoleSort(int* arr, int left, int right)//0,n
{
	//三数取中优化
	int mid = GetMidIndex(arr, left, right + 1);
	Swap(&arr[left], &arr[mid]);
	int keyi = arr[left];//存钥匙留坑
	int pit = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[left])
		{
			right--;
		}
		arr[pit] = arr[right];
		pit = right;//更新坑
		
		while (left < right && arr[left] <= arr[right])
		{
			left++;
		}
		arr[pit] = arr[left];
		pit = left;//更新坑
	}
	int nextkey = left;
	Swap(&arr[nextkey], &keyi);//此时keyi存的就是一开始的值了
	return nextkey;
}

 

前后指针法

其实几种方法本质上都是左右对半分的大思想,这里的话也是选择一个key,然后不同的是定义了两个指针,prev指针指向最左,cur指向后一个位置,然后cur找小,找到了比较prev有没有比key大,没有则两个指针同时加加,然后cur接着找,找到了,观察prev的下一个值,比key大,就交换两个指针的值。

 

int PointerSort(int* arr, int left, int right)//0,n
{
	int mid = GetMidIndex(arr, left, right + 1);
	Swap(&arr[left], &arr[mid]);
	int keyi = left;
	int prev = keyi;
	int cur = prev + 1;

	while (cur <= right)
	{
		if (arr[cur] < arr[keyi])
		{
			prev++;
			Swap(&arr[prev], &arr[cur]);			
		}
		/*if (arr[cur] < arr[keyi] && ++prev != cur)这一步更好更妙
			swap(&arr[prev], &arr[cur]);*/

		cur++;
	}
	Swap(&arr[prev], &arr[keyi]);
	return prev;
}
非递归的快排

这里的快排我是借助栈来实现的,栈的话,前面的文章我也有写,然后这篇文章后面的代码我也会一块分享出来,大家一起学习!

主要的话是借助栈,模拟快排实现递归的功能。

如果我们要排的是0-9,那么它的递归路线应该是这样的

栈的特质是后进先出,也可以说先进后出。

那么我们就把0和9入栈,然后在出栈,通过前面三种方法之一取得我们的数组中间的位置,再把右区间和左区间分别入栈,出栈,然后不断地调用获取key,多次调用方法,使得这个数组变成有序,达到模拟快排递归的思想和过程。

不排除在某些极端情况下可能还是会溢出,因为栈区的空间毕竟还是没有特别大。

void QuickNoNRSort(int* arr, int begin, int end)
{
	Stack qs;
	StackInit(&qs);

	StackPush(&qs, begin);
	StackPush(&qs, end);
	while (!StackEmpty(&qs))
	{

		//栈是先进后出,我们取到的顺序是先右后左
		int right = StackTop(&qs);
		StackPop(&qs);

		int left = StackTop(&qs);
		StackPop(&qs);
		int keyi = PointerSort(arr, left, right);
		//[left,keyi-1] keyi [keyi+1,right]

		if (right > keyi + 1)
		{
			StackPush(&qs, keyi + 1);
			StackPush(&qs, right);
		}
		if (keyi - 1 > left)
		{
			StackPush(&qs, left);
			StackPush(&qs, keyi - 1);
		}
	}
	StackDestroy(&qs);
}
6.冒泡排序(简单)

我这里说的简单,理解起来简单,实现起来也简单。

比方说第一个数,跟第二个数比,比第二个数大就交换,然后在跟第三个数比,第四个数比,循环往复,当遇到比它自己大的数就停止,然后比它大的那个数继续第二轮的比较,最后最大的那个数就排到了最后面。然后这里只需要加一个循环,那么每个数都可以排一次,最终每个数都去到了相应的位置。

第一趟j是0,那么进来i的循环,就是跑了一次的一趟循环。

这时候给他加一层循环,那么每个数都可以实现一次排序,n-1是因为数组下标从零开始。

void BubbleSort(int* arr, int n)
{//冒泡排序
	for (int j = 0; j < n - 1; j++)
	{
		for (int i = 0; i < n - 1-j; i++)
		{
			int tmp = arr[i + 1];
			if (arr[i] > arr[i + 1])
			{
				arr[i + 1] = arr[i];
				arr[i] = tmp;
			}
			else
			{
				break;
			}
		}
	}	
}
7.归并排序(分解再合并)

这个排序听起来有点麻烦,不过理解了之后会发现它实现起来是很舒服的。

比方说我们要排的是10,6,7,1,3,9,4,2.

那么我们可以先把他们切割成一半一半的,10,6,7,1一组,3,9,4,2一组。紧接着再切割,10,6一组,7,1一组,3,9一组,4,2一组。最后每个数分成一个组,那么我们就可以认为,这些一个数的组都是有序的。

接着我们再把两个两个一组组的合并排序,排好了之后四个一组,这两组四个数的组进行排序,就排好了我们一开始要排的这个数组。

 这里的话我们可以用一个临时的空间,我们的数组多大我们就开辟多大的临时空间,在这块临时空间里进行排序,最后将排好的值挨个拷贝回原来的数组即可。

接着我们把它实现一下,拷贝我用的是memcpy,这是c语言存在的 一个复制方法,使用的时候包含一下头文件即可,当然练习的时候不加也可以。

void _mergersort(int* arr, int begin, int end, int* tmp)
{
	if (begin >= end)//>=
		return;
	//确定区间
	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]
	//不断分解成左右两个区间
	_mergersort(arr, begin, mid, tmp);
	_mergersort(arr, 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 (arr[begin1] <= arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	//循环结束时哪个区间还有剩余数据,就把剩余数据续到后面
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	//最后把排好数据再拷回原数组(只拷贝当前归并的区间)
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

//归并排序
void mergersort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//申请临时空间存储排好的值
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//n*lognN,tmp不在递归空间内开辟。
	_mergersort(arr, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

这个的话写的话有点绕,但是用起来很舒服,大范围的数值也能够承受,时间复杂度仅仅n*logn。一般多用于外磁盘排序,比如说有10G的数值要排,那么把它们分成1G再排,不断细分再归并,会比其他的排序方法更胜一筹。这里我就提一嘴,具体的实现可以参考其他大佬的文章~

非递归版本

上面的话主要是分解再合并排序,那么我们只需要模拟它的分解以及合并排序就可以实现非递归版本。

这里的话,给定两个区间,我们这里有八个数,那就给八个区间,两两区间进行比较排序,排完了之后再把两组结合,就变成四个一组,最后再结合,就变成八个一组排好序的数组了。

那么怎么让两个变成四个呢,我们可以给定一个变量,假设是1,那么第一趟,我们给的两个区间都是只有一个值,我们进行判定排序,这是四组就是已经排好序的两个值。那么当这个值是2的时候,我们给的区间有2个值,这时候我们创建一个临时数组,在一个值时就将排好的值拷贝回去,第二次的时候,还是比较两个区间的第一个值,哪个小就放到临时数组里,然后这个区间和临时数组都++,因为已经比较和存值了。

这个时候如果有还没比较的值,因为我们有时比较的不一定是2的平方嘛,比如10,18那些。

那问题来了,如果我们要排的是 10个数,我们第一趟两两比较是可以的,那么第二趟四四比较,数组是不是就会存在越界的可能啊?

数组越界,那就会取到其他的值,这些值进来我们跟我们的值比较,那么最后这些值可能会拷贝回我们的数组,这个情况是要杜绝的。

我们上面用的是begin1和end1,begin2和end2这几个区间。我们分析一下,上面这种情况,begin2和end2会存在越界。如果是12个数,那么end1,begin2和end2会存在越界,因为是2的平方嘛。那么begin1会不会越界呢?答案是不会的,因为你想,如果刚好是2的平方,哪个都不会越界,如果出了一点,我们的循环是小于数组长度的,begin1怎么都不会越界。

我们只需要将越界的长度缩短,那就不会越界了,比如长度是n,那就缩短为长度n即可,相当于最后那几个值复制了一份,两份一样的值在比较而已。

//归并排序(非递归)
void MergerSortNonR(int* arr, 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 = 0; j < n; j += 2 * gap)
		{
			//归并
			int begin1 = j;
			int end1 = j + gap - 1;
			int begin2 = j + gap;
			int end2 = j + 2 * gap - 1;

			//判断越界的情况并进行处理
			//第一组部分越界
			if (end1 >= n)
				break;
			//end1=n-1;修正路线,整体拷贝必须修正
			//第二组全部越界
			if (begin2 >= n)
				break;
			//不存在区间
			//begin2 = n;修正路线,整体拷贝必须修正
			//第二组部分越界
			if (end2 >= n)
				end2 = n - 1;

			int i = j;
			//归并,取小的尾插(升序)
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] <= arr[begin2])
				{
					tmp[i++] = arr[begin1++];
				}
				else
				{
					tmp[i++] = arr[begin2++];
				}
			}
			//循环结束时哪个区间还有剩余数据,就把剩余数据续到后面
			while (begin1 <= end1)
			{
				tmp[i++] = arr[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = arr[begin2++];
			}
			//最后把排好数据再拷回原数组(只拷贝当前归并的区间)
			memcpy(arr + j, tmp + j, (end2 - j + 1) * sizeof(int));
		}
		
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}
8.计数排序

计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

假设我们要排这样一组数,其中最大的数值是5,我们就创建一个(k)5+1的数组,然后循环遍历数组,记录到5,就在下标为5的位置增加1,记录到4,就在下标为4的位置增加1,直到遍历完这个数组。

接着打印,记录下标为0的数组时,它有一个0,就打印一个0,当检测到下标为2的时候,此时数组记录是2,就会输出两个2,直到数组最后一个位置的数量打印完停止。

当然这个前提是数值之间差距不大时适用的,一个0一个9999这种十几个数就不太合适。

那么,如果是101,100,102,104,106这类大的值呢,岂不是要开辟100多个空间的数组?

并不是,我们数组的下标是从零开始,我们处理的值此时相差是不大的,只需要选出最小值和最大值,用最大值减去最小值,再加一就是我们需要开辟的空间数了。

这时我们可以想办法优化一下我们的思想:

    怎么优化呢?
    刚才我们统计次数其实是用的待排数据的一个绝对映射,什么意思呢?
    即待排的数据是几,它的次数就保存在了另一个数组下标为几的位置上。
    那现在要优化,我们可以考虑使用数据的相对映射来搞:
    什么意思呢?
    就拿这组数据来说:
    如果还像上面那样的话,我们就得开一个大小为106的数组,因为最大值是106嘛,0到106 就是106个数嘛。
    那这样前100个空间是不是都没用上啊。

那现在我们改变一下:

    上面这组数据最大值max是106,最小值min是100,所以我们只需开一个大小为(max-min+1)的数组就够了。
    那该数组的下标范围就是【0,6】了,如果和上面一样的话100就应该映射到下标为100的位置,那按我们现在的思路100是最小值,就映射到下标为0的位置。
    即对于待排数组中的数组arr[i],就映射到下标为arr[i]-min的位置就行了。

大家再思考一下:

    用计数排序如果待排数组中有负数可以吗?
    如果用绝对映射是不是不行,假如有个-6,绝对映射的话-6的次数应该存在下标为-5的位置,这不就越界了,数组下标是不能为负的。
    但我们现在用相对映射是不是就可以了。
    假如有个负数-6,是最小的一个,那它映射到哪个位置,-6-min即-6-(-6)=0


那其实这个优化的作用呢就是能少开一些空间。

memset可以将一定空间内的值定义成特定值,刚好我们能够全赋值为0.

 

//计数排序
void CountSort(int* arr, int n)
{
	assert(arr);
	//找最值
	int max = arr[0];
	int min = arr[0];
	for (int i = 1; i < n; i++)
	{
		if (arr[i] > max)
			max = arr[i];
		if (arr[i] < min)
			min = arr[i];
	}
	int range = max - min + 1;

	//创建数组
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * range);
	/*int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}*/

	//统计个数
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}//这里可能一开始看看不太懂,画画图会好理解一些。

	//排序(遍历count数组按个数往原数组放数据)
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		//count[i]就是每个数据出现的次数
		while (count[i]--)
		{
			arr[j] = i + min;
			j++;
		}
	}

	//释放malloc的数组
	free(count);
	count = NULL;
}
复杂度与稳定性

三、源码分享

stack.c
#define _CRT_SECURE_NO_WARNINGS 

#include"Stack.h"

void StackInit(Stack* ps)
{
	assert(ps);
	ps->array = (STDataType*)malloc(sizeof(STDataType) * 4);
	if (ps->array == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	ps->capacity = 4;
	ps->top = 0;
}
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	//assert( ! StackEmpty(ps));//!!大道至简,万法皆空
	/*printf("%d\n", ps->array[ps->top-1]);*/ //返回栈顶元素
	/*printf("%d\n", ps->top);*/ //返回栈顶下标,不是下标元素
	/*return ps->top - 1;*/ //返回的是栈顶下标的前一个,此时top是10,返回的是九。
	return ps->array[ps->top - 1];
}
int StackSize(Stack* ps)
{
	assert(ps);
	assert(ps->top);
	printf("%d\n", ps->top);
	return ps->top;
}
int StackEmpty(Stack* ps)
{
	assert(ps);
	//如果ps->top == 0成立,结果为true,表示栈空。
	//return !ps->top ;
	return ps->top == 0;
}
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	ps->array[ps->top] = data;
	ps->top++;
	/*printf("%d\n", ps->array[ps->top]);*/
	//检查栈满了需要扩容
	if (ps->top == ps->capacity)
	{
		STDataType* tmp = (STDataType*)realloc(ps->array, sizeof(STDataType) * ps->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(-1);
		}
		ps->array = tmp;
		ps->capacity *= 2;
	}

}
void StackPop(Stack* ps)
{
	assert(ps);
	/*assert(ps->top);*/
	/*assert(StackEmpty(ps));*/
	ps->top--;
}
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->array);
	ps->array = NULL;
	ps->top = 0;
	ps->capacity = 0;
}
stack.h
#define _CRT_SECURE_NO_WARNINGS 

#define _CRT_SECURE_NO_WARNINGS 

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
	STDataType* array;
	int top;		// 栈顶
	int capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
sort.c
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>

#include"Stack.h"

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void PrintSort(int* arr, int n)
{
	for (int i = 0; i < n ; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
void InsertSort(int* arr, int n)
{

	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)//这里每次都会再次排序,0排一次,1排两次
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else
			{
				break;
			}
			arr[end + 1] = tmp;
		}
		
	}
	
}

void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//只要大于1都是预排序,等于1就相当于插入排序,最后一步很重要,即使前面的预排序没有排很好,最后一下也会排好
		gap = gap / 2;
		/*gap = gap / 3 +1;*/
		for (int i = 0; i < n - gap ; i ++)//一步排序
		{
			int end = i;
			while (end >= 0)
			{
				int tmp = arr[end + gap];
				if (arr[end] > arr[end + gap])
				{
					arr[end + gap] = arr[end];
					--end;
				}
				else
				{
					break;
				}
				arr[end + 1] = tmp;
			}
		}
	}	
}

void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	/*int max = begin;
	int min = begin;*/
	//将 max 和 min 的定义放在 while 循环外部
	//这种情况下,max 和 min 的作用域跨越了整个 while 循环
	//它们的值在每次循环迭代时会被保留和更新     
	//将 max 和 min 的定义放在 while 循环内部
	//这种情况下,max 和 min 的作用域仅限于每次 while 循环迭代
	//当循环迭代结束时,它们的值会被销毁,下一次循环重新定义和初始化
	while (begin < end)
	{
		int max = begin;
		int min = begin;
		//在选择排序算法中,正确的做法是将 max 和 min 的定义放在 while 循环内部,以确保它们在每次循环迭代时都能被正确初始化和使用
		for (int i = begin + 1; i <= end; i++)
		{//第一趟begin是0,max,min也是0
			if (arr[i] > arr[max])
			{
				max = i;
			}
			if (arr[i] < arr[min])
			{
				min = i;
			}
		}
		Swap(&arr[min], &arr[begin]);
		if (max == begin)
		{
			max = min;
		}
		Swap(&arr[max], &arr[end]);
		begin++;
		end--;
	}
}

void BubbleSort(int* arr, int n)
{//冒泡排序
	for (int j = 0; j < n - 1; j++)
	{
		for (int i = 0; i < n - 1-j; i++)
		{
			int tmp = arr[i + 1];
			if (arr[i] > arr[i + 1])
			{
				arr[i + 1] = arr[i];
				arr[i] = tmp;
			}
			else
			{
				break;
			}
		}
	}	
}


void HeapDown(int* a, int n, int parent)//大堆,越来越xiao
{
int child = parent * 2 + 1;
while (child < n)
{
	if (a[child] < a[child + 1] && child + 1 < n)  //第一个箭头一换,就变逆序
	{
		child++;
	}
	if (a[parent] < a[child])//箭头一换,就变逆序
	{
		Swap(&a[child], &a[parent]);
		parent = child;//
		child = parent * 2 + 1;//
		/*HeapDown();*/ //向下排序
		/*HeapUp(); */ //向上排序
	}
	else
	{
		break;
	}
}
}
void HeapSort(int* a, int size)
{
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)
	{
		HeapDown(a, size, i);

		//这一步其实已经实现了降序排序
	}
	int count = size - 1;
	while (count > 0)
	{
		//加上这一步就是升序
		Swap(&a[0], &a[count]);
		HeapDown(a, count, 0);
		count--;
	}
}

int GetMidIndex(int* arr, int left, int right)
{

	int mid = (left + (right - left)) / 2;
	//ringht-left  /2

	if (left > right)
	{
		if (right > mid)
			return right;
		else if (mid > left)
			return left;
		else//mid < left && right < mid
			return mid;
	}
	else//left < right
	{
		if (right < mid)
			return right;
		else if (left > mid)
			return left;
		else//right > mid && left < mid
			return mid;
	}
}
int HoareSort(int* arr, int left, int right)//0,n
{
	//三数取中优化
	int mid = GetMidIndex(arr, left, right + 1);
	Swap(&arr[left], &arr[mid]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])
		{
			right--;
		}
		//这里有点惭愧,我一直在调用keyi,实际是arr[keyi],然后找了很久也没发现问题所在,细节还是很重要的
		while (left < right && arr[left] <= arr[keyi])//这里应该是循环不是判断,一直找到比它大才停止,不是只判断一次
		{
			left++;
		}

		if (left < right)
		{
			Swap(&arr[left], &arr[right]);
		}
		//++left;
		//--right;
	}
	int nextkey = left;
	Swap(&arr[nextkey], &arr[keyi]);
	return nextkey;
}

int HoleSort(int* arr, int left, int right)//0,n
{
	//三数取中优化
	int mid = GetMidIndex(arr, left, right + 1);
	Swap(&arr[left], &arr[mid]);
	int keyi = arr[left];//存钥匙留坑
	int pit = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[left])
		{
			right--;
		}
		arr[pit] = arr[right];
		pit = right;//更新坑
		
		while (left < right && arr[left] <= arr[right])
		{
			left++;
		}
		arr[pit] = arr[left];
		pit = left;//更新坑
	}
	int nextkey = left;
	Swap(&arr[nextkey], &keyi);//此时keyi存的就是一开始的值了
	return nextkey;
}

int PointerSort(int* arr, int left, int right)//0,n
{
	int mid = GetMidIndex(arr, left, right + 1);
	Swap(&arr[left], &arr[mid]);
	int keyi = left;
	int prev = keyi;
	int cur = prev + 1;

	while (cur <= right)
	{
		if (arr[cur] < arr[keyi])
		{
			prev++;
			Swap(&arr[prev], &arr[cur]);			
		}
		/*if (arr[cur] < arr[keyi] && ++prev != cur)这一步更好更妙
			swap(&arr[prev], &arr[cur]);*/

		cur++;
	}
	Swap(&arr[prev], &arr[keyi]);
	return prev;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if( end-begin <= 10)
	{
		InsertSort(arr, end +1 );//因为传过来是下标的最大值,所以这里加一才是数组长度
		return arr;
    }
	int key = HoareSort( arr, begin, end);

	/*int key = PointerSort(arr, begin, end);*/
	QuickSort(arr, begin, key - 1);
	//这里有点惭愧,我一直在调用keysort,然后找了很久也没发现问题所在,细节还是很重要的
	QuickSort(arr, key + 1, end);

}

一趟快速排序 [left,right]
//int PartSort(int* arr, int left, int right)
//{
//	int keyi = left;
//	while (left < right)
//	{
//		//R先走,向左找小
//		while (left < right && arr[right] >= arr[keyi])
//		{
//			right--;
//		}
//		//L向右找大
//		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* arr, int begin, int end)
//{
//	if (begin >= end)
//		return;
//	int keyi = PartSort(arr, begin, end);
//	//[begin,keyi-1] keyi [keyi+1,end]
//
//	//排keyi左边
//	QuickSort(arr, begin, keyi - 1);
//	//排keyi右边
//	QuickSort(arr, keyi + 1, end);
//}

void QuickNoNRSort(int* arr, int begin, int end)
{
	Stack qs;
	StackInit(&qs);

	StackPush(&qs, begin);
	StackPush(&qs, end);
	while (!StackEmpty(&qs))
	{

		//栈是先进后出,我们取到的顺序是先右后左
		int right = StackTop(&qs);
		StackPop(&qs);

		int left = StackTop(&qs);
		StackPop(&qs);
		int keyi = PointerSort(arr, left, right);
		//[left,keyi-1] keyi [keyi+1,right]

		if (right > keyi + 1)
		{
			StackPush(&qs, keyi + 1);
			StackPush(&qs, right);
		}
		if (keyi - 1 > left)
		{
			StackPush(&qs, left);
			StackPush(&qs, keyi - 1);
		}
	}
	StackDestroy(&qs);
}

void _mergersort(int* arr, int begin, int end, int* tmp)
{
	if (begin >= end)//>=
		return;
	//确定区间
	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]
	//不断分解成左右两个区间
	_mergersort(arr, begin, mid, tmp);
	_mergersort(arr, 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 (arr[begin1] <= arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	//循环结束时哪个区间还有剩余数据,就把剩余数据续到后面
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	//最后把排好数据再拷回原数组(只拷贝当前归并的区间)
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

//归并排序
void mergersort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//申请临时空间存储排好的值
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//n*lognN,tmp不在递归空间内开辟。
	_mergersort(arr, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

//归并排序(非递归)
void MergerSortNonR(int* arr, 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 = 0; j < n; j += 2 * gap)
		{
			//归并
			int begin1 = j;
			int end1 = j + gap - 1;
			int begin2 = j + gap;
			int end2 = j + 2 * gap - 1;

			//判断越界的情况并进行处理
			//第一组部分越界
			if (end1 >= n)
				break;
			//end1=n-1;修正路线,整体拷贝必须修正
			//第二组全部越界
			if (begin2 >= n)
				break;
			//不存在区间
			//begin2 = n;修正路线,整体拷贝必须修正
			//第二组部分越界
			if (end2 >= n)
				end2 = n - 1;

			int i = j;
			//归并,取小的尾插(升序)
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] <= arr[begin2])
				{
					tmp[i++] = arr[begin1++];
				}
				else
				{
					tmp[i++] = arr[begin2++];
				}
			}
			//循环结束时哪个区间还有剩余数据,就把剩余数据续到后面
			while (begin1 <= end1)
			{
				tmp[i++] = arr[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = arr[begin2++];
			}
			//最后把排好数据再拷回原数组(只拷贝当前归并的区间)
			memcpy(arr + j, tmp + j, (end2 - j + 1) * sizeof(int));
		}
		
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}

//计数排序
void CountSort(int* arr, int n)
{
	assert(arr);
	//找最值
	int max = arr[0];
	int min = arr[0];
	for (int i = 1; i < n; i++)
	{
		if (arr[i] > max)
			max = arr[i];
		if (arr[i] < min)
			min = arr[i];
	}
	int range = max - min + 1;

	//创建数组
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * range);
	/*int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}*/

	//统计个数
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}//这里可能一开始看看不太懂,画画图会好理解一些。

	//排序(遍历count数组按个数往原数组放数据)
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		//count[i]就是每个数据出现的次数
		while (count[i]--)
		{
			arr[j] = i + min;
			j++;
		}
	}

	//释放malloc的数组
	free(count);
	count = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>

#include"Stack.h"



void QuickSortTest()
{
	int array[] = { 9,1,2,5,7,4,8,6,3,5 };

	/*mergersort(array,  sizeof(array) / sizeof(int) );*/
	/*MergerSortNonR(array, sizeof(array) / sizeof(int));*/
	//QuickSort(array, 0 , sizeof(array)/sizeof(int)  -1);
	QuickNoNRSort(array, 0, sizeof(array) / sizeof(int) - 1);
	PrintSort(array, sizeof(array) / sizeof(int));
}
int main()
{
	//int array[] = { '9','8','7','6','5','4','3','2','1','0' };
	// 上面这里我用的是字符表示,所以是用sacll编码表示出来的
	//0是48,1是49这样排列,原数字是这样写就可以-> 
	int array1[] = {9,8,7,6,5,4,3,2,1,0 };
	InsertSort(array1, sizeof(array1) / sizeof(int) );
	PrintSort(array1, sizeof(array1) / sizeof(int));
	
	int array2[] = { 9,8,7,6,5,4,3,2,1,0 };
	ShellSort(array2, sizeof(array2) / sizeof(int));
	PrintSort(array2, sizeof(array2) / sizeof(int));

	int array3[] = { 9,8,7,6,5,4,3,2,1,0 };
	SelectSort(array3, sizeof(array3) / sizeof(int));
	PrintSort(array3, sizeof(array3) / sizeof(int));

	int array4[] = { 9,8,7,6,5,4,3,2,1,0 };
	BubbleSort(array4, sizeof(array4) / sizeof(int));
	PrintSort(array4, sizeof(array4) / sizeof(int));

	/*int array4[] = {0, 1,2,3,4,5,6,7,8,9 };
	HeapSort(array4, sizeof(array4) / sizeof(int));
	PrintSort(array4, sizeof(array4) / sizeof(int));*/

	//QuickSortTest();
	int array[] = { -5,-2,-1,1,5,2,4,3,0};
	CountSort(array, sizeof(array) / sizeof(int));
	PrintSort(array, sizeof(array) / sizeof(int));

	return 0;
}

才疏学浅,三天打鱼两天晒网,拖了很久才重新提起记笔记的习惯,没想到吧哈哈哈哈,我的本意是自己看的,分享反倒成了其次~有什么写的不对的还请指正!一起学习一起进步~

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值