排序【数据结构】

文章目录

  • 一、 稳定性
  • 二、排序
    • 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为待排序序列的长度。由于每次插入都需要移动元素,所以对于较大的数据集来说,直接插入排序可能效率较低。

直接插入排序是稳定

排升序

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

如图;
在这里插入图片描述

代码展示

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)。

如图:
在这里插入图片描述

代码展示

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) 直接选择排序

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

需要注意的是,直接选择排序中存在大跨度的数据移动,
是一种不稳定的排序方式
原因是:
在这里插入图片描述

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

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

图1:

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

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

在这里插入图片描述

代码展示

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

//选择排序
//时间复杂度;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),它是不稳定排序算法。
不稳定的原因 例如
在这里插入图片描述

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

代码展示

//堆排序

//交换两个数
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;
    }
    
③ 快速排序的非递归方式

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

代码展示

//非递归快速排序
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直到某一指针超出序列尾。将另一序列剩下的所有元素直接复制到合并序列尾。

在这里插入图片描述

代码展示

//归并排序子函数
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. 归并排序是稳定的

归并排序的非递归方式
思路:先一个一个(单独一个默认有序)数据归并成两个有序,然后再两个两个归并成四个有序,… ,直到整体有序
如图:
在这里插入图片描述
代码展示

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)不稳定

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/356912.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

给刚上小学的侄女准备新年礼物,有什么让小朋友喜欢的玩具推荐?

给刚上小学的侄女准备新年礼物&#xff0c;我觉得也是有很多选择的。因为现在的市场上款式太多了&#xff0c;选择自己心意的适合小侄女的都是可以的。但是如果非要选益智的或是智能高科技的&#xff0c;对孩子来说既能玩耍又能在玩的同时学习到知识&#xff0c;能够开拓孩子眼…

用httpd服务搭建公司公用的资源下载服务器

最新产品有些新发布的项目版本下载资源。过往是存在git上面的。但由于版本号、资源文件过大、资源分类等因素。很不方便。因此&#xff0c;想到以前到官网下载第三方jar包时&#xff0c;直接打开Linux目录的方法。查了下&#xff0c;用httpd可以作到。 效果如下图&#xff1a; …

人事经理HR快速提升个人能力,依据法律法规搞定企业劳动纠纷

一、教程描述 入职当月社保尚无法缴纳&#xff0c;发生工伤怎么办&#xff1f;拿不出离职证明的员工&#xff0c;HR到底能不能要&#xff1f;“不能胜任工作”能否炒人不用赔钱&#xff1f;如何运用协商解除劳动合同&#xff0c;化解相关不稳定因素造成的风险&#xff1f;本套…

Spring Cloud+SkyWalking全链路监控部署及使用分享

先了解 SkyWalking 极简入门 | Apache SkyWalking 版本&#xff1a;apache-skywalking-apm-9.7.0.tar.gz OAP服务和UI服务 apache-skywalking-java-agent-9.1.0.tgz JAVA-AGENT服务 环境&#xff1a;linux 项目&#xff1a;spring cloud 记录下碰到的问题&#xff1a; 1、s…

保障气膜建筑稳定性的关键因素与方法

近年来&#xff0c;气膜建筑因其轻便、柔韧、环保等特点在建筑领域备受瞩目。然而&#xff0c;作为一种依赖气体支撑的结构&#xff0c;如何确保气膜建筑的稳定性成为一个重要的问题。本文将探讨保障气膜建筑稳定性的关键因素与方法&#xff0c;从气压差维持、材料选择、锚固系…

【Javaweb程序设计】【C00164】基于SSM的飞机订票系统(论文+PPT)

基于SSM的飞机订票系统&#xff08;论文PPT&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目包运行、免费远程调试 项目简介 这是一个基于ssm的飞机订票系统 本系统分为前台用户模块和后台管理员模块。 前台用户模块&#xff1a;当游客打开系统的网址后&#xff0…

产品经理必备资料:从入门到精通,助您提升专业技能

​ 你是否曾经感到自己在产品开发过程中缺乏足够的知识和技能&#xff1f;你是否曾经花费大量时间在网上搜索各种资料&#xff0c;却依然无法满足自己的需求&#xff1f;现在&#xff0c;我们为你提供了一份全面的产品经理资料&#xff0c;让你在产品开发道路上更加顺畅&#x…

快速上手Git

目录 一、Git概述 二、Git的常用命令 Git全局配置 获取Git仓库 基本概念 本地仓库操作 远程仓库操作 分支操作 标签操作 三、在IDEA中使用Git 在IDEA中配置Git 本地仓库操作 远程仓库操作 分支操作 冲突解决 一、Git概述 Git是一个分布式版本控制工具&…

【Linux】fork()函数

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

wordpress找不回密码怎么办?4种方法设置新密码

有些WordPress站长太久不登录后台了&#xff0c;所以就忘记了管理员登录密码&#xff0c;这种情况我们应该怎么找回密码呢&#xff1f;或者设置一个新密码呢&#xff1f;下面boke112百科就跟大家分享4种方法设置WordPress新密码。 方法一、登录页面的“忘记密码&#xff1f;”…

16. 输入设备应用编程

16. 输入设备应用编程 1. 输入类设备编程介绍1.1 什么是输入设备1.2 input 子系统1.3 读取数据的流程1.4 应用程序如何解析数据 2. 读取 struct input_event 数据3. 按键应用编程4. 触摸屏应用编程4.1 解析触摸屏设备上报的数据4.1.1 单点触摸设备——事件上报顺序4.1.2 多点触…

消息中间件RabbitMQ介绍

一、基础知识 1. 什么是RabbitMQ RabbitMQ是2007年发布&#xff0c;是一个在AMQP(高级消息队列协议)基础上完成的&#xff0c;简称MQ全称为Message Queue, 消息队列&#xff08;MQ&#xff09;是一种应用程序对应用程序的通信方法&#xff0c;由Erlang&#xff08;专门针对于大…

Unix环境高级编程-学习-04-匿名管道PIPE

目录 一、环境 二、介绍 三、C标准函数介绍 1、pipe 2、popen 3、pclose 4、注意 四、宏 五、常见的管道用法 1、一对一&#xff08;父进程读子进程写一条管道&#xff09; 2、一对一&#xff08;父进程写子进程读一条管道&#xff09; 3、一对多&#xff08;父进程…

代码随想录算法训练营DAY6 | 哈希表(1)

DAY5休息一天&#xff0c;今天重启~ 哈希表理论基础&#xff1a;代码随想录 Java hash实现 &#xff1a;java 哈希表-CSDN博客 一、LeetCode 242 有效的字母异位词 题目链接&#xff1a;242.有效的字母异位词 思路&#xff1a;设置字典 class Solution {public boolean isAnag…

微搭低代码从入门到精通02数据源的介绍

目录 1 数据源的功能组成2 在低码编辑器中调用数据源的能力3 视频讲解 一款低代码产品好不好用&#xff0c;数据建模的能力是一个重要的衡量指标。因为灵活的定义表之间的关系&#xff0c;自由的选择字段的类型&#xff0c;尤其在我们依据模型自动生成页面的时候是比较重要的。…

Windows Server 2025 LTSC 预览版来了

Windows Server 2025 LTSC 预览版来了 1. 安装 Windows Server 2025 LTSC 预览版2. 安装 VMware Tools3. Windows Server 2025 LTSC 预览版4. Windows Server 2025 LTSC 预览版下载地址 1. 安装 Windows Server 2025 LTSC 预览版 使用 VMware Workstation 安装&#xff0c; 安…

JVM实战(30)——模拟堆内存溢出

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 学习必须往深处挖&…

JVM内存调优常用参数

视频讲解地址 文章目录 一、开始二、常用命令1、原生命令2、arthas命令 三、Parallel四、G1 相关参数五、通用参数六、JVM调优参数 一、开始 查看当前JDK版本所支持的垃圾回收器有哪些、以及默认使用的回收器 java -XX:PrintFlagsFinal -version | grep -E \<Use.*GC\>J…

类和对象(2)之类的6个默认成员函数(2)

上次我们梳理了初始化和清理的知识点&#xff0c;今天我们要梳理的是拷贝赋值的知识点。 拷贝构造函数 看到拷贝构造函数这个名字就能看的出来它是一个构造函数&#xff0c;所以它的语法和构造函数很相似。 既然他是一个构造函数&#xff0c;那么他就具有构造函数的语法&…

前端颜料盘??

前端颜料盘&#xff1f;&#xff1f; 一、原生颜料盘 <input type"color" placeholder"选择颜色">二、第三方开源库 Pickr&#xff1a; GitHub: https://github.com/Simonwep/pickr官方网站: https://simonwep.github.io/pickr/Pickr 是一个轻量级…