排序【数据结构】

文章目录

  • 一、 稳定性
  • 二、排序
    • 1. 插入排序
      • (1) 直接插入排序
      • (2) 希尔排序
    • 2. 选择排序
      • (1) 直接选择排序
      • (2) 堆排序
    • 3. 交换排序
      • (1) 冒泡排序
      • (2) 快速排序
        • ① 普通版快排
        • ② 关于优化快排
        • ③ 快速排序的非递归方式
    • 4. 归并排序
    • 5. 计数排序
  • 三、 总结

一、 稳定性

在计算机科学中,稳定性是指在排序过程中,相等的元素的相对顺序保持不变。也就是说,如果元素a和b在排序之前是相等的,那么在排序之后,a和b的相对顺序应该和排序之前一样;否则不稳定。

二、排序

1. 插入排序

(1) 直接插入排序

直接插入排序是一种简单的排序方法,它的基本操作是将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录数量增1的有序表。

具体操作步骤如下:

  1. 从未排序的序列中选择一个元素,将其插入到已排序序列的合适位置。
  2. 继续从未排序序列中取出下一个元素,然后将其插入到已排序序列的合适位置。
  3. 重复步骤2,直到所有元素都被插入到已排序序列中。

直接插入排序的时间复杂度为O(n ^ 2),其中n为待排序序列的长度。由于每次插入都需要移动元素,所以对于较大的数据集来说,直接插入排序可能效率较低。

直接插入排序是稳定

排升序

这里采用的基本思想是: 先假设第一个元素是有序的,然后从后方进行插入元素,然后再与前面的元素进行比较,当然前提是把要插入的元素临时存放一下(避免元素移动后,给覆盖了)。如果比前面的元素小,就要把前面的元素后移,直到找到比前面的大或者第一个位置,然后把要插入的元素放进去。

如图;
排序【数据结构】_第1张图片

代码展示

void InsertSort(int* arr,int n) 
{
	//升序
	//从末端开始
	//先假设第一个元素 有序
	//第二元素从从末端进行插入,与前面的元素比较
	//使用末端元素,如果比前面的小,前面的元素进行后移
	//之后再把元素进行插入
	int i = 0;
	for (i = 0; i < n-1;i++)
	{
		int end = i;	
		int tmp = arr[end + 1];	//临时存放插入的元素
		while (end >=0)
		{
			//小于前的,就让前面的元素后移
			if (tmp < arr[end]) 
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else 
			{
				//当大于等于的时候直接跳出循环
				break;
			}
		}
		//把要插入的元素放进去
		arr[end + 1] = tmp;
	}
}

(2) 希尔排序

希尔排序是直接插入排序算法的一种更高效的改进版本,它是插入排序的一种,也称为“缩小增量排序”,因 D.L.Shell 于 1959 年提出而得名。

希尔排序的基本思想是:现将待排序的数组分成多个待排序的子序列,使得每个子序列的元素较少,然后对各个子序列分别进行插入排序,待到整个待排序的序列基本有序的时候,最后在对所有的元素进行一次插入排序。

也就是:分为 预排序 和 插入排序

  • 预排序:这以升序为例,根据间隔(gap)分为多个子序列进行插入排序,排完后,就把较大的数据放在后面,较小的数据放在前面,这样就变为部分有序。
    关于这里的gap的取值:
    gap越大,大的值更快调到后面,小的值更快调到前面,接近有序的速度越慢
    gap越小,跳的越慢,但是越接近有序。gap == 1 相当于插入排序
  • 一次插入排序:一次插入排序后,变成整体有序。

希尔排序是不稳定的,(原因是:相同的数据可能被分在不同的组,前后数据的位置难以控制 ) 希尔排序的时间复杂度取决于间隔序列的选择。理论上,如果间隔序列是逐一减半的,希尔排序的时间复杂度可以接近O(n log n)。但是,如果间隔序列选择不当,可能会导致最坏情况的时间复杂度为O(n^2)。
所以,希尔排序的时间复杂度通常是在O(n log n)和O(n ^ 2)之间,具体取决于间隔序列的选择。希尔排序的时间复杂度约是O(n^1.3)。

如图:
排序【数据结构】_第2张图片

代码展示

void ShellSort(int* arr ,int n) 
{
	
	int gap = n;
	while(gap > 1)
	{
		gap = gap / 3 + 1;	//一组子序列中的每个数的间距
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			//插入排序,尾插进行排序
			int tmp = arr[end + gap];	//记录尾部的数据
			while (end >= 0)
			{
				// 升序
				if (tmp < arr[end])
				{
					//后面的小于前面的向后移动
					arr[end + gap] = arr[end];
					end-=gap;	//注意是子序列
				}
				else
				{
					//后面的大于前的直接插入
					break;
				}
			}
			//把排序的数据放进去
			arr[end + gap] = tmp;
		}
	}
}

2. 选择排序

(1) 直接选择排序

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

需要注意的是,直接选择排序中存在大跨度的数据移动,
是一种不稳定的排序方式
原因是:
排序【数据结构】_第3张图片

时间复杂度是O(n ^ 2),最好的情况是O(N ^ 2),最坏的情况是O(N ^ 2); 空间复杂度为O(1)。

假设,排升序,这里我们用数组存储数据,那就是要遍历数组进行直接选择排序(选出较小的依次从数组的起始位置开始排)

图1:

因为选择排序要么选出最大的,要么选出最小的。

  • 首先根据数组的个数,来确定数组存放最小值的地方和最大值的地方(升序,数组最左端存放小值[begin],数组最右端存放大值[end])
  • 再使用maxi和mini下标来进行记录一轮中的最大值的下标和最小值的下标
  • 找到下标后,把mini和maxi指向的值与begin和end进行交换
  • 交换后,begin++;end - -; 一轮排好了,缩小数组范围,再进行下一轮遍历。

排序【数据结构】_第4张图片

代码展示

选择排序,选出大的和选出小的同时进行

//选择排序
//时间复杂度;O(N^2)
//最好的情况:O(N^2)
void SelectSort(int* arr, int n) 
{
	int begin = 0;
	int end = n - 1;	//数组最后一个元素的下标
	while (begin < end)
	{
		//把大的数放到最右边,小的数放到最左边
		//不断向中间缩小范围
		int maxi = begin, mini = begin;
		for (int i = begin+1; i <= end;i++)
		{

			if (arr[i] < arr[mini])
			{
				//把下标给mini
				mini = i;
			}
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
		}
		//一轮找完后
		//把大的数放到最右边,小的数放到最左边
		swap(&arr[begin], &arr[mini]);
		//上述的swap 就是 把小的值与左端begin进行交换
		//需要注意的是:当左端begin的下标与maxi(那一趟认为较大的数的下标)相等时,
		//maxi == begin ,swap较换后把小值放到了begin,而maxi下标也是指向begin
		//当再把maxi指向的数据放到右边的时候,maxi之前指向的数已经让上面swap给换了,换到mini所指向的值了
		//所以需要加个判断给换回来
		if (begin == maxi)
		{
			maxi = mini;
		}
		swap(&arr[end], &arr[maxi]);
		++begin;
		--end;
	}
}

选出小的依次进行排序

//选小进行排序
void SelectSort(int* arr, int n)
{
	int begin = 0;
	while (begin < n)
	{
		int mini = begin;
		for (int i = begin; i < n; i++)
		{
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		swap(&arr[begin], &arr[mini]);
		++begin;
	}
}

选出大的从后往前放进行排序

//选大进行排序
void SelectSort(int*arr,int n)
{
	int end = n - 1;
	while (end >= 0) 
	{
		int maxi = end;
		for (int i = end; i >= 0;i--)
		{
			//寻找较大的下标
			if (arr[i] > arr[maxi]) 
			{
				maxi = i;
			}
		}
		swap(&arr[end],&arr[maxi]);
		--end;
	}
}

(2) 堆排序

这里以升序为例。我们需要把数据先构建成大堆(根的值最大),然后把堆的根元素与堆最后面一个元素进行交换。然后堆的大小减一。

依次重复上述。直到排完。

堆排序的时间复杂度为O(nlogn),它是不稳定排序算法。
不稳定的原因 例如
排序【数据结构】_第5张图片

关于堆排序,猛击链接 堆排序 更详细的介绍

代码展示

//堆排序

//交换两个数
void swap(int*s1,int*s2) 
{
	int tmp = *s1;
	*s1 = *s2;
	*s2 = tmp;
}

//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
	//先去找根结点的较大的孩子结点
	int child = 2 * parent + 1;
	//可能会向下调整多次
	while (child<size) 
	{
		//这里使用假设法,先假设左孩子的值最大
		//如果不对就进行更新
		if ((child+1 < size)&&a[child] < a[child+1]) 
		{
			child++;
		}
		//根结点与其孩子结点中的较大的一个进行交换
		if(a[child] > a[parent]) 
		{
			swap(&a[child],&a[parent]);
			//更新下标
			parent = child;
			child = 2 * parent + 1;
		}
		else 
		{
			break; //调完堆
		}
	}
}

//堆排序
void HeapSort(int* arr, int n) 
{
	int i = 0;
	//使用向下调整算法向上调整,把大的值调到上方。
	for (i = (n - 1 - 1) / 2; i >= 0;i--)
	{
		//先找到数组最后端的父结点的下标
		//父结点的下标减一就是另一个
		//使用向下调整算法进行调整
		AdjustDown(arr,n,i);
	}

	//进行排序
	//因为是大堆,所以根结点的值是最值
	//把最值与堆的最后一个结点进行交换
	//再把交换后的根节点进行向下调整
	//然后堆的大小减一
	

	//注意end 是从n-1开始的(数组最后一个元素的下标)
	int end = n-1;
	while (end > 0) 
	{
		//swap end = n-1 这表示下标
		swap(&arr[0],&arr[end]);
		//adjustdown 函数里面的end是元素的个数,所以不是先--end
		//所以
		AdjustDown(arr,end,0);
		end--;
	}
}

3. 交换排序

(1) 冒泡排序

冒泡排序的基本思想是:对相邻的元素进行两两比较,顺序相反则进行交换,这样每一次遍历都将最大的元素"浮"到数列的最后,下一次遍历则考虑剩下的元素。通过多次遍历,可以将整个数列排序。

  • 如果是n个数据元素,需要遍历n-1趟,在第一趟排序中,需要比较的次数为n-1次。在第二趟排序中,需要比较的次数为n-2次,以此类推。

代码展示

void BubbleSort(int* arr,int n) 
{
	//遍历n-1趟
	for (int i = 0; i < n - 1;i++) 
	{
        //每一趟交换的次数
		for (int j = 0; j < n - 1 - i;j++) 
		{
			//冒泡升序
			if (arr[j] > arr[j+1])
			{
				swap(&arr[j], &arr[j + 1]);

			}
		}
	}
}

冒泡排序总结:

  1. 冒泡排序是稳定的
  2. 是一种交换类排序
  3. 时间复杂度,最坏的情况:O(n^2) ; 最好的情况:可以达到O(n)

最好情况就是,数据本身是有序的,在去遍历一遍时通过记录值来判断有序,当整体有序直接跳出循环

如下方:

void BubbleSort(int* arr,int n) 
{
	for (int i = 0; i < n-1;i++)
	{
		//使用记录值
		bool exchange = false;
		for (int j = 0; j < n - 1 - i;j++) 
		{
			if (arr[j] > arr[j+1])
			{
				swap(&arr[j],&arr[j+1]);
				//发生了交换,说明不是有序
				exchange = true;
			}
		}
		//当上方循环一遍,exchange仍为false,说明整体有序直接跳出循环
		//这样最好的情况时间复杂度可以达到O(N)
		if (exchange == false)
			break;
	}
}

(2) 快速排序

快速排序是一种被广泛运用的排序算法,它的基本原理是分治法。具体来说,就是通过趟排序将待排序的序列分割为左右两个子序列,左边的子序列中所有数据都比右边子序列中的数据小,然后对左右两个子序列继续进行排序,直到整个序列有序。

  1. 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)。
  2. 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大。
  3. 递归地对左右两个子序列进行快速排序,直至序列有序。

快速排序的平均时间复杂度为O(nlogn),最坏情况下为O(n^2)。快速排序在处理大量数据时效率较高。但是快速排序是不稳定的,快速排序在处理相同元素时,它们的相对位置可能会改变。例如,对序列 9 6 9 1 2 3 进行排序,第一趟排序后变为 3 6 9 1 2 9,原本第一个9的位置发生了变化,因此快速排序是不稳定的。

所以这里先 根据基准 key 来进行分左右子序列, 基准的选择先固定选数组的第一个元素,然后找左右下标,从数组左边找大,数组右边找小。左右两边找到后进行交换,当left == right,在把基准进行交换。

① 普通版快排
void QuickSort(int* arr, int begin,int end) 
{
	//区间只有一个结点或者区间不存在,停止递归
	if (begin >= end)
		return;

	int keyi = begin;
	int left = begin, right = end;
	while (left<right)
	{
		//右边找小
		while (left<right && arr[right] >= arr[keyi])
		{
			right--;
		}
		//左边找大
		while (left<right && arr[left]<= arr[keyi]) 
		{
			left++;
		}
		//找到后进行交换
		swap(&arr[left],&arr[right]);
	}

	//交换基准
	swap(&arr[left],&arr[keyi]);

	//递归左右子序列
	keyi = left;

	//[begin,keyi-1]keyi[keyi+1,end]
	QuickSort(arr,begin,keyi-1);
	QuickSort(arr,keyi+1,end);
}
② 关于优化快排
  1. 三数取中法:在开始(begin)、中间(mid)、最后(end)这三个位置中选出中间大(即中位数)的那个数确定下标为midi。

进行数据的交换,把中间大的数据仍然交换放到数组的左端,作为基准进行排序。时间复杂度为O(n*logn)
代码展示

void swap(int* s1 ,int* s2) 
{
	int tmp = *s1;
	*s1 = *s2;
	*s2 = tmp;
}

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

int GetMidi(int* arr, int begin,int end) 
{
	int midi = (begin + end) / 2;
	if (arr[begin] < arr[midi]) 
	{
		if (arr[midi] < arr[end])
		{
			return midi;
		}
		else if (arr[begin] > arr[end])
		{
			return begin;
		}
		else
			return end;
	}
	else 
	{
		if (arr[midi] > arr[end])
		{
			return midi;
		}
		else if (arr[begin] < arr[end])
		{
			return begin;
		}
		else 
		{
			return end;
		}
	}
}

void QuickSort(int* arr,int begin,int end) 
{
	//只有一个结点的时候直接进行返回
	if (begin >= end)
		return;

	//优化部分,三数取中
	int midi = GetMidi(arr,begin,end);
	//把中位数放到基准的位置
	swap(&arr[midi],&arr[begin]);

	//选基准
	int left = begin;
	int right = end;
	int keyi = begin;

	while (left < right) 
	{
		//数组的右边的数据先走
		// 左右两边向中间进行聚拢
		//右边走遇到比基准小的值就停下,左边走遇到大于基准的就停下
		//都停下后,交换左右两边的数
		while (left<right && arr[right] >= arr[keyi])
		{
			right--;
		}
		while (left<right && arr[left] <= arr[keyi])
		{
			left++;
		}
		//找到后就就进行交换
		swap(&arr[left],&arr[right]);
	}

	//当上方一轮遍历完后
	//即left == right
	//在相遇点与基准keyi进行交换
	swap(&arr[keyi],&arr[left]);
	keyi = left;

	
	//进行递归快排左右子序列
	//[begin,keyi-1]keyi[keyi+1,end]
	QuickSort(arr,begin,keyi-1);
	QuickSort(arr,keyi+1,end);
}
  1. 小区间优化法

    	//当排序元素数据量很小的时候直接调用插入排序
    	if (end - begin+1 < 10) 
    	{
    		InsertSort(arr+begin, end - begin + 1);
    	}else
        {
            ... //调用快速排序
        }
    
  2. 挖坑法
    因为快排在进行完单趟排序后,然后去递归左右子区间的单趟排序,这针对之前hoare版本的单趟排序进行优化

    首先,在区间的开头左边的值记录一下(key),然后用坑位下标(holei)记录此数据。左边就是坑位;右边找小填到左边的坑中,之后,右边就形成了新的坑;左边找大填到右边的坑中;当begin == end 就结束,最后把key记录的值填到最后的坑里面去。

    //挖坑法
    int QuickSortPart2(int* arr, int begin, int end) 
    {
    	//三数取中
    	int midi = GetMidi(arr, begin, end);
    	swap(&arr[begin], &arr[midi]);	//交换基准值
    	int key = arr[begin];
    	int holei = begin;
    	while (begin < end)	//相遇停止
    	{
    		//右边找小
    		while (begin < end && arr[end] >= key)
    		{
    			--end;
    		}
    		//找到后填到左边的坑中,这样就把比基准小的值放到左边
    		arr[holei] = arr[end];
    		//此时右边形成新的坑位了,更新坑位下标
    		holei = end;
    
    		//左边找大
    		while (begin<end && arr[begin] <= key)
    		{
    			++begin;
    		}
    		//找到填到右边的坑
    		arr[holei] = arr[begin];
    		holei = begin;
    	}
    	//相遇把key记录的值放进去
    	arr[holei] = key;
    	return holei;
    }
    
  3. 前后指针法

    单趟进行优化,首先,一个指针指向开头(prev),一个指针指向其后(cur)。当cur遇到比key小的值,++prev交换prev和cur位置的值,再++cur。当cur遇到比key大的值,++cur。
    如图:

    //双指针法(前后指针法)
    int QuickSortPart3(int* arr, int begin, int end) 
    {
    	//三数取中
    	int midi = GetMidi(arr, begin, end);
    	swap(&arr[begin], &arr[midi]);	//交换基准值
    	int keyi = begin;
    	//首先一个指针指向开头,另一个指向其后面那个
    	int prev = begin;
    	int cur = prev + 1;
    
    	//while (cur <= end )
    	//{
    	//	//当cur遇到比key大的值,++cur
    	//	if (arr[cur] > arr[keyi])
    	//	{
    	//		++cur;
    	//	}
    	//	else
    	//	{
    	//		//cur遇到比key小的值,++prev,交换prev和cur位置的值,再++cur
    
    	//		++prev;
    	//		swap(&arr[prev],&arr[cur]);
    	//		++cur;
    	//	}
    	//}
    	//swap(&arr[prev], &arr[keyi]);
    	//return prev;
    
    	//优化一
    	//while ( cur <= end) 
    	//{
    	//	if (arr[cur] < arr[keyi])
    	//	{
    	//		++prev;
    	//		swap(&arr[prev],&arr[cur]);
    	//	}
    	//	++cur;
    	//}
    	//swap(&arr[prev],&arr[keyi]);
    	//return prev;
    
    	//优化二
    	while (cur <= end) 
    	{
    		if (arr[cur] < arr[keyi] && ++prev != cur)
    			swap(&arr[prev],&arr[cur]);
    		++cur;
    	}
    	swap(&arr[prev],&arr[keyi]);
    	return prev;
    }
    
③ 快速排序的非递归方式

递归改为非递归我们需要借助 数据结构的栈来进行实现。借助出栈来取出每一次的区间进行处理。分出两段区间,处理完之后的入栈条件是:区间合理(左<=右),当左>右不入栈。当不再有区间进栈后(栈为空时) 结束。
如图:
排序【数据结构】_第6张图片

代码展示

//非递归快速排序
void QuickSortNonR(int* arr, int begin, int end) 
{
	Stack st;
	InitStack(&st);
	//栈 后进先出
	
	//区间压栈 
	//要想让区间的左值先出,选要后进栈
	PushStack(&st,end);
	PushStack(&st,begin);

	while (!EmptyStack(&st))
	{
		//出栈 找出区间的左右下标,进行处理
		int left = TopStack(&st);
		PopStack(&st);
		int right = TopStack(&st);
		PopStack(&st);

		// 使用了双指针法进行了一次快速排序
		int keyi = QuickSortPart3(arr, left, right);

		//左右子区间 [left,keyi-1] keyi [keyi+1,right]

		//区间合理(左<=右),当左>右不入栈。当不在有区间进栈后(栈为空时) 结束

		//左区间满足条件
		if (left < keyi - 1)
		{
			//后进先出
			//左子区间的右下标进栈
			PushStack(&st, keyi - 1);
			//左子区间的左下标进栈
			PushStack(&st, left);
		}

		//右区间满足条件
		if (keyi+1 < right) 
		{
			//后进先出
			//右子区间的右下标进栈
			PushStack(&st,right);
			//右子区间的左下标进栈
			PushStack(&st,keyi+1);
		}
	}
	DestroyStack(&st);
}

4. 归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效且稳定的排序算法,主要思想是将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。依次归并达到整体有序。
若将两个有序表合并成一个有序表,称为二路归并。具体过程为:

  • 申请空间(辅助空间),使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
  • 重复步骤3直到某一指针超出序列尾。将另一序列剩下的所有元素直接复制到合并序列尾。

排序【数据结构】_第7张图片

代码展示

//归并排序子函数
void _MergeSort(int* arr,int begin,int end,int* tmp) 
{
	//当区间只有一个结点或区间不存在,停止递归,返回调用的地方
	if (begin >= end)
		return;
	
	//找到区间中间点
	//分成左右两个区间进行递归
	//[begin,mid][mid+1,end]
	int mid = (begin + end) / 2;
	_MergeSort(arr,begin,mid,tmp);
	_MergeSort(arr,mid+1,end,tmp);

	//有序归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;

	//归并时,有一边结束就结束了,两个都没有结束就继续
	int i = begin;	//注意 归并到指定的位置去
	while (begin1 <= end1 && begin2 <= end2) 
	{
		//取小数据尾插到tmp数组
		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,sizeof(int)*(end-begin+1));

}

//归并排序
void MergeSort(int* arr,int n) 
{
	//归并排序,先分解成单个有序,再进行有序归并
	//借助辅助空间O(n)
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//开辟成功调用子函数,进行归并
	_MergeSort(arr,0,n-1,tmp);
	free(tmp);
}

归并排序总结:

  1. 时间复杂度是:O( n*log(n) ) ,因为需要借助辅助空间,所以空间复杂度为O( n )
  2. 归并排序是稳定的

归并排序的非递归方式
思路:先一个一个(单独一个默认有序)数据归并成两个有序,然后再两个两个归并成四个有序,… ,直到整体有序
如图:
排序【数据结构】_第8张图片
代码展示

void MergeSortNR(int* a, int n) 
{
	//非递归方式实现归并排序

	//申请辅助空间
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	//二路归并:两个有序子序列 归并为一个有序的序列
	int gap = 1;	//先单个元素有序
	while (gap < n) 
	{

		//一层归并
		int i = 0;
		for (i = 0;i<n;i+= 2*gap)
		{
			int begin1 = i, end1 = i+gap-1;
			int begin2 = i+gap, end2 = i+2*gap-1;
			if (end1 >= n || begin2 >= n) 
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			int j = begin1;	//注意 这里是从i 因为归并不是一定全在下标0开始的,也有可能从右边的子数组开始的
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
					tmp[j++] = a[begin1++];
				else
					tmp[j++] = a[begin2++];
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}

			//一组归并后,立即拷贝
			memcpy(a+i,tmp+i,sizeof(int)*(end2-i+1));	//注意放在里面
		}

		gap *= 2;
	}
	free(tmp);
}

5. 计数排序

计数排序的思想:

  • 统计每个数据出现的次数
  • 将统计出现的次数放入一个临时数组(采用相对映射的方式-将数据的最小值放到第一个数据)

代码展示

void CountSort(int* a, int n)
{
	//排升序
	//第一步 相对映射的的方式找出最小值和最大值
	int i = 0;
	int min = a[0], max = a[0];
	for (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*)calloc(range,sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}

	//第三步 统计数据出现的次数
	i = 0;
	for(i = 0;i<n;i++)
	{
		count[a[i] - min]++;	// 相对映射
	}

	//第四步 将临时数组记录的数据有序放入数组a中
	//注意 因为是相对映射,所以这里要加上min(放入数组a的时候)
	int j = 0;
	i = 0;
	for (j = 0; j < range;j++) 
	{
		//因为相同数据可能会出现多次
		//没有出现一次的元素,临时数组存放0
		while(count[j]--)
		{
			//有数据就放入a数组中
			a[i++] = j + min;
		}
	}

	//第五步 释放内存
	free(count);
}

计数排序的优缺点:

  • 缺点:不适合分散的数据,更适合集中的数据,数据类型只适合整数
  • 优点:效率极高,O(N+cntN)
  • 时间复杂度:O(N+range)
  • 空间复杂度:O(range)

三、 总结

排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 稳定
选择排序 O(n^2) O(n^2) O(n^2) O(1) 不稳定
直接插入排序 O(n^2) O(n) O(n^2) O(1) 稳定
希尔排序 O(n*log(n))~O(n^2) O(n^1.3) O(n^2) O(1) 不稳定
堆排序 O(n*log(n)) O(n*log(n)) O(n*log(n)) O(1) 不稳定
归并排序 O(n*log(n)) O(n*log(n)) O(n*log(n)) O(n) 稳定
快速排序 O(n*log(n)) O(n*log(n)) O(n^2) O(log(n))~O(n) 不稳定

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