数据结构——排序(C语言)

目录

一、插入排序

二、希尔排序

三、选择排序

四、堆排序

五、冒泡排序

六、快速排序

        1.hoare版

2.挖坑法

快排优化1:三数取中选key

快排优化2:当递归区间比较小的时候就不再递归用快排方法排序。可以用其他排序处理小区间。

3.前后指针法

4.前后指针法优化版本(三数取中和小于10数用插入排序)

4.用栈实现快排

七、归并排序

1.递归版

2.非递归版(循环版)

八、计数排序


 

 

一、插入排序

 

0-end有序,在end+1位置插入一个数。tmp记录下n+1下标的数。从下标为n开始向前遍历数组,遍历到的数就是每次要和tmp比较的数。


升序:
    1.这个数比tmp大,把这个数往后挪一个位置,然后下标向前,找下一个要比较的数。
    2.这个数比tmmp小,把tmp放该数后面(0-end 本身就有序,所以如果tmp比end小那就直接放到最后,不用跟前面的数比较)

 

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

最差情况:逆序

最优情况:顺序有序或者接近顺序有序。123456,每个数据都是直接插入到后面,一个都没挪。

最优时间复杂度:O(N)

//插入排序
void InsertSort(int *a, int n)//升序
{
	//0-end有序,在end+1位置插入一个数。tmp记录下n+1下标的数。从下标为n开始向前遍历数组,遍历到的数就是每次要和tmp比较的数。
	//升序:
	//1.这个数比tmp大,把这个数往后挪一个位置,然后下标向前,找下一个要比较的数。
	//2.这个数比tmmp小,把tmp放该数后面(0-end 本身就有序,所以如果tmp比end小那就直接放到最后,不用跟前面的数比较)
	for (int i = 0; i < n - 1; i++)//外层套个循环,内层只是让一个新来的数插入,使数据有序。
	{
		int end = i;
		int tmp = a[end + 1];//用tmp存储想要插入的数
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp; // 2.这个数比tmmp小,把tmp放该数后面。不能在else位置写,因为有可能走完while循环(一直在调换位置,没进入过else)。end<0,end的下一个位置是这组数的头。
	}

}

 


二、希尔排序

 

升级版的插入排序

1.预排续(接近顺序有序)
   分组,每间隔gap的数据为一组。在一组中的数都间隔gap,在原位置排序。
2.直接插入排序

 

排升序,gap越小,越接近有序,gap越大,越不接近有序。gap==1的时候直接插入排序,数组有序。

那gap要怎么设计?

gap = gap / 3 + 1;

保证最后一次(gap==1)是直接插入排序,和之前的分组预排不一样。

 

//希尔排序
void ShellSort(int* a, int n)
{
	//1.预排续(接近顺序有序)
	//分组,每间隔gap的数据为一组。在一组中的数都间隔gap,在原位置排序。
	//2.直接插入排序
	int gap=n;
	while(gap>1)
	{
		gap = gap / 3 + 1;//保证最后一次(gap==1)是直接插入排序,和之前的分组预排不一样
		
			for (int i = 0; i < n - gap; i++)//用来更新end,n-gap是这组数的最后一个数, 所以i < n - gap(i是用来控制end的),最后一次插入n-gap位置的数
			{
				int end = i;
				int tmp = a[end + gap];//用tmp存储想要插入的数,0-end有序,插入end的后一个数,就是end+gap
				while (end >= 0)
				{    
					if (tmp < a[end])
					{
						a[end + gap] = a[gap];
						end -= gap;
					}
					else
					{
						break;
					}
					a[end + gap] = tmp;

				}
			}
		
	}
}

 

时间复杂度:6cb2bd7bd6014e308af7a91f2cee87a4.png(精确答案)

因为太复杂了所以我们只进行估算:N*2dcd9ee2138142da942f78fd028444b2.png

如果gap每次 /3+1,那就以3为底

如果gap每次 /2,那就以2为底

 

分析如下:

1.第一层循环:
a794a09e05314a0b98d95b358ee59a6d.png

N/3/3/3/3...==1

3^X==N

X=2dcd9ee2138142da942f78fd028444b2.png

 

2.第二层循环:
数据结构——排序(C语言)_第1张图片

 

拿第一次举例子 gap=N/3....

循环n-gap次也就是 3/2 N,时间复杂度就是N

 

3.第三层循环:

数据结构——排序(C语言)_第2张图片

 

 gap越大,里面的循环几乎可以忽略不计。gap非常大的时候,一步跳N/3,一组数可能是3个,所以第三层循环可以忽略不记。

当gap很小,里面的循环几乎可以忽略不计。gap是1或者2,数列已经很接近有序了,不会进行太多次的调换,很快就break出循环。

 


三、选择排序

直接选择排序

 

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

 

2. 稳定性:不稳定

//直接选择排序
void Swap(int * a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void SelectSort(int * a, int n)
{
	assert(a);

	int begin = 0, end = n - 1;//遍历的过程中选出一个最大数一个最小数,最小数放到左边,最大数放到右边。begin++,end--,缩小范围往中间逼近
	while (begin < end)//奇数个begin==end的时候就结束。偶数个end a[max_i])
			{
				max_i = i;
			}
		}

		//走完之后找到begin和end之间 最大的和最小的数

		Swap(&a[begin], &a[min_i]);

		//max在begin位置的时候就出问题了。先调换min和begin。然后你找到的max位置的数就被换走了
		//修正一下
		if (begin == max_i)
		{
			max_i = min_i;
		}
		Swap(&a[end], &a[max_i]);
		begin++;
		end--;
	}

 }

 

插入排序和直接选择排序哪个更好?插入排序

插入排序最优的情况是O(N)  直接选择排序是O(N^2)。当数列部分有序的时候插入排序是很有优势的。

 


四、堆排序

 

首先要建堆。向上调整建堆比较复杂,使用向下调整建堆。

升序建大堆,降序建小堆。

void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;//先假设左孩子是小的那个

	while (child a[child + 1])//完全二叉树,最后一个节点可能没有兄弟节点,child+1下标会超出数组范围,形成越界访问。所以增加条件child+1=0;i--)
{
    AdjustDown(a,n,i);
}
}

 


五、冒泡排序

 

时间复杂度:O(N^2) 最好的情况是O(N)                                        

//冒泡
void BubbleSort(int *a, int n)
{
	assert(a);
	for (int j = 0; j < n; j++)//第一次把最大的数换到最后,第二次把次大的数放到倒数第二个位置                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
	{
		int exchange = 0;
		for (int i = 1; i < n; i++)
		{
			if (a[i - 1] > a[i]) //相邻两个元素比较
			{
				Swap(&a[i - 1], &a[i]);//前一个大的话就交换位置,把大的换到后面。
				exchange = 1;
			}	
		}

		//相邻的数两两相比,exchange==0时,说明没有发生任何一次调换,即数列已经有序,跳出循环,不需要再继续了
		if (exchange == 0)
		{
			break;
		}
	}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
}

 


六、快速排序

 

   1.hoare版

 

//快排
int PartSort1(int *a, int begin, int end)//霍尔版
{
	int left = begin, right = end;
	int keyi = left;//存储key的下标
	while (left < right)//等于的时候跳出
	{
		//右边先走,找比key小的数,找到一个停止
		while (a[right] >= a[keyi] && left < right)
		{
			right--;
		}
		//左边再走,找比key大的数
		while (a[left] <= a[keyi] && left < right)
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}

	Swap(&a[keyi], &a[left]);//left==right 下标是哪个都行
	keyi = left;//key现在在left下标位置,更新一下keyi

	return keyi;
}   

void QuikSort(int *a, int begin, int end)
{
	//区间不存在或者只有一个值不需要再处理
	if (begin >= end)
	{
		return;
	}

	int keyi = PartSort1(a, begin, end);

	QuikSort(a, begin, keyi - 1);//递归 
	QuikSort(a, keyi + 1, end);

}

1.    选出key值,可以选最左边也可以选最右边

        左边作key,右边先走

        右边作key,左边先走    (后面有解析)

2.    排升序的情况:

单趟排完之后,左边的数比key小,右边的数比key。我们选左边第一个数作为key值。定义left和right变量,右边先走,比key大就就继续走,每次找到比key小的数就停止不再继续往前走。左边后走,找到比key大的数就停止。left和right位置的数交换。直到left和right相遇,结束。将key与相遇位置的数交换。key就到了它应该在的位置。更新keyi(key的下标)

3.    将数列分为区间:begin——keyi-1  |  keyi   |  keyi+1——end

void QuikSort(int *a, int begin, int end);

使用递归的方式 调整区间左边 ,再右边。

有两种情况不用继续调整,区间没有数(begin==end)或区间只有一个数(begin>end)。

  • 数列只有 0 1 位置两个元素,begin==0,end==1
  • 假设经过上一轮调整交换,1位置为key,左区间只有一个数,右区间没有数。
  • keyi=1  左区间: 0(begin)——0(keyi-1)    右区间:1(keyi+1)——0(end) 

while (a[right] >= a[keyi] && left < right)

等于key的时候要进去。防止出现死循环

5 5 .........5。一直不进去就会死循环

left

 

为什么left和right一定会相遇而不是错开?

4aad5a28ce684e07b63b238e37790bb3.png                  e82dea04325c4a349e0deec775da653c.png          

 这个是直接选择排序的部分代码。在这个while循环里,begin和end是同时往中间逼近的。每次一步。所以有可能相遇,也有可能错开。

 

而我们这个版本的快速排序是右边先走,左边再走。一方在走的时候一方是停止不动的。所以他们一定会相遇。

 

为什么左边作key就要从右边先走?

我们排升序,左边应该是小的数。在这个版本的排序中left和right相遇时,需要交换keyi位置(左边第一个数)和相遇位置的数。 排升序,需要左边都比key小,右边都比key大 。

相遇位置的数要去左边第一个,所以一定要是比key小的数。 

左边做key右边先走是为了保证相遇位置的数是比key小的数或者就是key的位置。           

情况一:R先走,停下来。L再走,遇到了R。R在的位置就是比key小的数。

情况二:R先走,一直没有找到比key小的数,R遇见了L。

在这里面又分了两种情况:

1.R直接走到了key的位置,L没动过,L和R相遇。相遇位置就是key应该在的位置

2.R先走,找到一个小数,停止,L再走,找到一个大数,停止。交换两数。这时候L所在的位置放的是一个比key小的数。进入下一次循环,R先走,再也没有遇到比key小的数了,R去和L相遇。相遇位置的数就是比key小的数

 

2.挖坑法

//挖坑法
int PartSort2(int *a, int begin, int end)
{
	int key = a[begin];
	int piti = begin;
	while (begin < end)//相遇时跳出,大体思路和霍尔版差不多
	{
		while (begin < end && a[end] >= key)//找到大的往前走,找到小的停止
		{
			end--;
		}

		//找到一个数放到坑里,并更新坑的位置
		a[piti] = a[end];
		piti = end;

		while (begin < end && a[begin] <= key)//找到小的往后走,找到大的停止
		{
			begin++;
		}

		//找到一个数放到坑里,并更新坑的位置
		a[piti] = a[begin];
		piti = begin;
	}

	//begin和end相遇,相遇位置一定是坑,因为end和begin其中一个一定是坑。
	//把key填到坑里
	a[piti] = key;
	return piti;

}
void QuikSort(int *a, int begin, int end)
{
	//区间不存在或者只有一个值不需要再处理
	if (begin >= end)
	{
		return;
	}

	int keyi = PartSort2(a, begin, end);

	QuikSort(a, begin, keyi - 1);//递归 
	QuikSort(a, keyi + 1, end);

}

     

大体思路还是和hoare版一样。左边作key从右边开始先走。左边比key小的数,找到停止。把这个数放到坑里,该数位置形成新坑,更新坑位置。右边再走,找比key大的数,找到放坑里,更新坑位置。直到end和begin相遇。把key放到相遇位置的坑里。  

 

挖坑法和霍尔版两种方法在排序过程中可能排出来的不一样。选择题可能出不定项选择,所以只明白一种思路是不够的。

例:

7 3 8 9 1 2

挖坑法:第一次排出来   2 3 1 7 9 8

霍尔版:第一次排出来   1 3 2 7 9 8

 


快排优化1:三数取中选key

数列有序或接近有序,如果选固定位置(最左边或最右边第一个作key),key每次都是排序后的最大数或最小数。

N +N-1+N-2+...1

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

快排问题2:数据量大的时候还容易栈溢出

解决方法:

1.随机选key

2.三数取中。第一个 中间 最后一个。选不是最大也不是最小的那个

 

int GetMidIndex(int *a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[end] < a[begin])
		{
			return begin;
		}
		else if (a[mid] < a[end])
		{
			return mid;
		}
		else
		{
			return end;
		}
	}

	else//a[begin]>=a[mid]
	{
		if (a[end] > a[begin])
		{
			return begin;
		}
		else if (a[mid] > a[end])
		{
			return mid;
		}
		else
		{
			return end;
		}

	}
}

 

 

快排优化2:当递归区间比较小的时候就不再递归用快排方法排序。可以用其他排序处理小区间。

区间小于10个数时,不再递归,可以减少80%以上的递归次数。使用插入排序。

在sort.h文件中声明  extern int CallCont;

对下面的前后指针法快排进行优化举例

 


3.前后指针法

//前后指针法
int PartSort3(int * a, int begin, int end)
{
	int keyi = begin;
	int prev = begin;
	int cur = begin + 1;

	while (cur <= end)
	{
		//cur找到的数比key小就停下,然后prev往前走一步,再把prev位置的大数和cur位置的小数交换。prev位置一直是大小数的分界线。调整结束后prev包括prev之前的全是小数,除了第一个的key数值。prev之后的全是大数。
		while (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}

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

	return keyi;
}

                                         

  cur找到的数比key小就停下,然后prev往前走一步,再把prev位置的大数和cur位置的小数交换。prev位置一直是大小数的分界线。调整结束后prev包括prev之前的全是小数,除了第一个的key数值。prev之后的全是大数。

 

 

4.前后指针法优化版本(三数取中和小于10数用插入排序)

//插入排序
void InsertSort(int *a, int n)//升序
{
	//0-end有序,在end+1位置插入一个数。tmp记录下n+1下标的数。从下标为n开始向前遍历数组,遍历到的数就是每次要和tmp比较的数。
	//升序:
	//1.这个数比tmp大,把这个数往后挪一个位置,然后下标向前,找下一个要比较的数。
	//2.这个数比tmmp小,把tmp放该数后面(0-end 本身就有序,所以如果tmp比end小那就直接放到最后,不用跟前面的数比较)
	for (int i = 0; i < n - 1; i++)//外层套个循环,内层只是让一个新来的数插入,使数据有序。
	{
		int end = i;
		int tmp = a[end + 1];//用tmp存储想要插入的数
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp; // 2.这个数比tmmp小,把tmp放该数后面。不能在else位置写,因为有可能走完while循环(一直在调换位置,没进入过else)。end<0,end的下一个位置是这组数的头。
	}

}


//前后指针法  加入三数取中的优化
int PartSort3(int * a, int begin, int end)
{
	CallCount++;
	int keyi = begin;
	int prev = begin;
	int cur = begin + 1;

	int  midi = GetMidIndex(a, begin, end);
	Swap(&a[keyi], a[midi]);

	while (cur <= end)
	{
		//cur找到的数比key小就停下,然后prev往前走一步,再把prev位置的大数和cur位置的小数交换。prev位置一直是大小数的分界线。调整结束后prev包括prev之前的全是小数,除了第一个的key数值。prev之后的全是大数。
		while (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}

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

	return keyi;
}


void QuikSort(int *a, int begin, int end)
{
	//区间不存在或者只有一个值不需要再处理
	if (begin >= end)
	{
		return;
	}
	if (end - begin > 10)
	{
		int keyi = PartSort3(a, begin, end);

		QuikSort(a, begin, keyi - 1);//递归 
		QuikSort(a, keyi + 1, end);
	}

	else
	{
		InsertSort(a, end - begin + 1);
    }
}

以上是递归版快排。

 


递归的问题:深度太深会栈溢出

1.改成循环

2.用数据结构模拟——用栈或队列都行

 

4.用栈实现快排
 

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

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

		int right = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, left, right);//调整一次,二分区间
		//left——keyi-1  |  keyi   |  keyi+1——right

		if (keyi + 1 < right)//区间有数
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);//先入左还是入右都一样。先入左再入右,取栈顶的时候就是右->左。根左右
		}
	}

	StackDestroy(&st);
}

 

栈是像前序遍历。

队列像层序遍历。

 


七、归并排序

 

1.递归版

 

有点像后序,先不断往下递归,到头了开始归并,再往上走。

归并两个有序数组。双指针,创建新数组归并,再拷贝回原数组。

从开始的1 1归并成2数一组。2 2再归并,成4数一组......最终所有数都被归并成有序的。

 

时间复杂度:N*logN   

深度logN,每一层不管是一个一个归,还是两两归,遍历数组时加起来大概都是循环N次来把数组走完。递归里面套循环,所以就是乘。

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,end1=mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin1;//i是tmp下标。不一定是从0开始。因为归并的两个组可能是 8-9 和 10-11
	while (begin1 <= end1 && begin2 <= end2)//有一组走完就会跳出
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}

	while (begin1 <= end1)//走到这一步有可能两组数都拷完了。也有可能只有一组先走完,没走完的一组本来就是有序的,剩下的数也有序,直接挨个往tmp拷就行。
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)//这两个循环,谁没走完谁进来,只能进来一个。至于到底哪个没走完,都写上就不用思考了。
	{
		tmp[i++] = a[begin2++];
	}

	//拷回原数组a
	memcpy(a, tmp, (end - begin1 + 1) * sizeof(int));

}

void MergeSort(int *a, int n)
{
	int *tmp = (int *)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

 

2.非递归版(循环版)

用栈?跟快排一样?用栈去模拟适合前序(队列适合层序)不适合后序。快排是选出一个key,找出左区间和右区间。归并是先分区间,往回返的时候开始归并。用栈的话,先把区间入进去,又拿出来继续处理分割。当要归并的时候就找不到区间了。

递归最终分出来的区间一定是一个数,一个数。我们自己用循环先两两归并->变成两两有序

归完了把数据拷贝回去

把两两有序的一组和另一组再进行归并  ->  每组四个数有序

四数一组再和另一组归并  ->  每组八个数有序

........

 

1.数组总大小不是2的次方倍都会越界。

1 3 5 2 7 6这组数一 一归并时没问题。二二归并,【1 3】和【5 2】归并。如果还是按照计算【7 6】和越界的一组数归并。

所以我们需要通过调整边界来防止越界

 

2.循环的归并并不是严格的二分。

10,6,7,1,3,9,4,2,5,6 (共10个数)

下标0-7一组,那右边区间按照严格二分应该是8-15。但右区间真的有这么多数吗?实际只有两个数。 在循环归并中,几个跟几个归都有可能。4个和2个。4个和4个。

除了begin1(由i控制,所以在数组下标范围内)。end1,begin2,end2都有可能越界。


如果区间只剩一个数了还需要归并吗? 
10, 6,7 ,1,3,9,4,2,5 (共9个数)

下标:【0 1】-【2 3】   【4 5】-【6 7】   【8 9】-【10 11】             (9-11越界)

取决于是tmp整体往回拷贝,还是归并一次就往回拷贝一次的局部拷贝。

整体拷需要。通过归并的过程把数放在tmp里,每组归并都结束后,最后把tmp完全拷到a,如果这个数没有进行归并过程的话,就没有放到tmp里,tmp里放的就是随机值,拷到a里也拷回去一个随机值。                                                                               

局部拷不需要。

所以两种方法对于越界的处理是不一样的。

 

怎么修正?           

10, 6,7 ,1,3,9,4,2,5 ,6(共10个数)                                                                                                                                         

下标:【0 - 3】  - 【4 -7】      【8 -11】 -【12- 15】     

end1越界,begin1和end1也越界。如果这样修正:end1=n-1,begin2=end2=n-1。

【8 9】-【9 9】 是不行的

这样处理相当于2个数和1个数归并。(把9位置的数多归并了一次,相当于给数组又加了一个和9位置数相等的数进行归并)。但tmp大小有限,我们不知道8 9位置谁大谁小,也就不能保证放进去的是有序的8和9,还是两个9 9。 

 

整体拷:

数据结构——排序(C语言)_第3张图片

 

局部拷:

数据结构——排序(C语言)_第4张图片

 

 整体拷:

void MergeSortNonR(int *a, int n)
{
	int *tmp = (int *)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)//一个gap最多就是n-1个数,这时右区间没有数
	{
		for (int i = 0; i < n; i += 2 * gap)//跳两个gap(一个小组中的两组数已经归并完成),进行下一小组的归并
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap-1;

			//越界修正边界
			if (end1 >= n)
			{
				end1 = n - 1;
				//将右区间修正成不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)//必须是else if。因为如果是else,前期没越界也会进去修正一下。
			{
				end2 = n - 1;
			}

			//归并
			int j = begin1;//j是tmp下标。不一定是从0开始。因为归并的两个组可能是 8-9 和 10-11
			while (begin1 <= end1 && begin2 <= end2)//有一组走完就会跳出
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)//走到这一步有可能两组数都拷完了。也有可能只有一组先走完,没走完的一组本来就是有序的,剩下的数也有序,直接挨个往tmp拷就行。
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)//这两个循环,谁没走完谁进来,只能进来一个。至于到底哪个没走完,都写上就不用思考了。
			{
				tmp[j++] = a[begin2++];
			}

		}
		memcpy(a, tmp, sizeof(int)*n);
		gap *= 2;

	}

	free(tmp);
}

局部拷:

void MergeSortNonR(int *a, int n)
{
	int *tmp = (int *)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)//一个gap最多就是n-1个数,这时右区间没有数
	{
		for (int i = 0; i < n; i += 2 * gap)//跳两个gap(一个小组中的两组数已经归并完成),进行下一小组的归并
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap-1;

			//end1越界或begin2越界,就可以不归并了
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			//局部拷要记录个数
			int m = end2 - begin1 + 1;

			//归并
			int j = begin1;//j是tmp下标。不一定是从0开始。因为归并的两个组可能是 8-9 和 10-11
			while (begin1 <= end1 && begin2 <= end2)//有一组走完就会跳出
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)//走到这一步有可能两组数都拷完了。也有可能只有一组先走完,没走完的一组本来就是有序的,剩下的数也有序,直接挨个往tmp拷就行。
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)//这两个循环,谁没走完谁进来,只能进来一个。至于到底哪个没走完,都写上就不用思考了。
			{
				tmp[j++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, sizeof(int)*m);
		}
		//memcpy(a, tmp, sizeof(int)*n);//整体拷在这里
		gap *= 2;

	}

	free(tmp);
}

 


八、计数排序

统计a数组每个数出现的次数,按出现次数写回原数组。

 

采用相对映射:100 ,100,102,103,104,105

cout 位置0下标表示该数是100

1下标表示101

2下标表示102

.........

count数组 2 0 1 1 1 1
count数组下标 0 1 2 3 4 5
count数组下标所映射的数 100 101 102 103 105 105

找a数组中最大数和最小数,算出数值范围,开辟一个能映射出所有a数组数的count数组。

将a数组里存储的数和count数组的下标建立映射关系,并用count数组存储该数的出现次数。count下标位置存储该数的个数,下标加min就是该数的大小。

 

时间复杂度:O(max(n,range))

找大小O(N),统计次数O(N),回写排序O(range)

空间复杂度:O(range)

 

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

	int range = max - min + 1;
	int *count = (int*)malloc(sizeof(int)*range);//开辟一个计数数组
	if(count==NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	memset(count, 0, sizeof(int)*range);//全都初始化成0

	//统计次数
	//这个数是几,映射的位置就是该数减min——相对映射。绝对映射:这个数是几就放到几的位置。
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	//回写排序
	//count下标位置是该数的个数,下标加min就是该数的大小
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		//count数组,i下标位置的数村的是几就往回写几次,下标加min就是该数的大小
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}

}

 

 

你可能感兴趣的:(数据结构,排序算法,数据结构,算法)