八大经典排序(详解+代码)

PS:以下所有排序都是以升序为例子。

需要用到的自定义Swap函数为:

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

冒泡排序

冒泡排序就如名字所诉,每一趟排序将冒出一个最大值。

代码如下:

void BubbleArray(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int t = 0; t < n - i-1; t++)
		{
			if (a[t] > a[t + 1])
			{
				Swap(&a[t], &a[t + 1]);
			}
		}
	}
}

直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

例如我们打扑克牌时,就是用了插入排序的思想。

我们不知道该数据哪里开始有序,所以我只能从第一个数据开始,不断把它往后排列,变成一个有序的序列

//直接插入排序代码
void InsertArray(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end+1]=a[end];
			}
			else
			{
				break;
			}
			end--;
		}
		a[end + 1] = tmp;
	}
}

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

希尔排序

希尔排序又称为缩小增量排序法,实际上就是直接插入排序的升级版

其基本思想是:

  1. 先定义一个小于数据数量N的增量gap,然后将所有距离为gap的数据分进同一组中,并且对同一组的数据进行一个直接插入排序,不断循环直到将所有分组都进行完一次插入排序;
  2. 缩小距离gap,增加分组不断循环;
  3. 直到gap为1时,就是将所有的数据直接使用一次直接插入排序,变为有序。

为什么要设定一个gap值?

答案:

        设定gap值能够使数据前面的大数更快的放到后面,后面的小数更快地放到前面,减少挪动次数,使得每一次插入排序后,数据更加接近于有序,能够很好的提高我们的运行速度

代码如下:

//希尔排序
void ShellArray(int* a, 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 = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end = end-gap;
				}
				else
				{
					break;
				}
			}
			a[end+gap] = tmp;
		}
	}
}

希尔排序的时间复杂度不好计算,因为gap值不是一个定值,是一直在变化的一个数,gap的取值带来的增益也是在变化的。

 时间复杂度暂时按照:O(n^1.25)到O(1.6*n^1.25)来算。

 

选择排序

选择排序基本思想:

从待排序列中每一次选出一个最小值,然后将起放到首位,直到全部数据排完即可。

//选择排序(一次选一个数)
void SelectSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		int start = i;
		int min = start;//记录最小元素的下标
		while (start < n)
		{
			if (a[start] < a[min])
				min = start;//更新最小值的下标
			start++;
		}
		Swap(&a[i], &a[min]);
	}
}

 进阶思想:

我们可以每次选两个数,一个最小值,一个最大值,每一次选完后都将它们放到首尾位置,这样能将运行效率能够提高一倍。

//选择排序(一次选两个数)
void SelectSort(int* a, int n)
{
	int left = 0;//记录参与该趟选择排序的第一个元素的下标
	int right = n - 1;//记录参与该趟选择排序的最后一个元素的下标
	while (left < right)
	{
		int minIndex = left;//记录每一趟最小值和最大值下标
		int maxIndex = left;
		int i = 0;
		//找出最大值及最小值的下标
		for (i = left; i <= right; i++)
		{
			if (a[i] < a[minIndex])
				{minIndex = i;}
			if (a[i]>a[maxIndex])
				{maxIndex = i;}
		}
		//将最大值和最小值放在序列开头和末尾
		Swap(&a[minIndex], &a[left]);
		if (left == maxIndex)
		{
			maxIndex = minIndex;//防止最大值位于序列开头,被最小值交换
		}
		Swap(&a[maxIndex], &a[right]);
		left++;
		right--;
	}
}

不过一次选两个数的方法有一个坑要注意!

如果你待排序列中首位数据是最大值的话一定要加上这个代码:

if (left == maxIndex)
        {
            maxIndex = minIndex;
        }

这是为了防止左边的值与最小值互换后,指向最大值的maxindex下标仍是首位数据,那就会变成最小值放到最右边,导致排序错误。

堆排序

想要使用堆排序,你需要先完成两个步骤,一是建堆,二是处理堆,需要学习堆的向下调整方法。

堆的向下调整方法——若想堆是大堆,那根节点的左右子树必须也是大堆;反之如果想要堆是小堆,那根节点的左右子树必须也是小堆。

需要注意的是:如果想要升序排序就要用大堆,想要降序排序就要使用小堆!

我们先看一下向下调整的代码:

//向下调整代码(大堆)
void AdjustDowm(int*a,int n,int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

讲解三点:

  1. 我们并不知道根节点的左右子树哪个大,那不妨我们就假设左子树大,我们可知左子树下标=根节点下标*2+1;
  2. 然后再比较左右子树哪个大,让child指向较大的子树;
  3. 比较父亲和孩子的大小,进行交换,重新指定父亲和孩子。

建堆代码如下:

void HeaptArray(int* a, int n)
{
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AdjustDowm(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDowm(a, end, 0);
		end--;
	}
}

讲解两点:

  1. i=(n-2)/2其实相当于i=(n-1-1)/2,n-1是最末尾数据的下标,n-1-1/2是为了找它的父亲结点进行向下调整,然后再不断进行递归,保证整个堆建立完成后,是一个大堆;
  2. 我们要知道,大堆只限定父子的关系,不会限定兄弟结点的关系,我们不会知道哪个子结点大哪个子结点小,所以我们只能不断将最大的数调到根结点处,然后再将其与最末尾的数据进行交换,这样才能保证是一个升序序列。

堆排序的时间复杂度

        当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。

堆排序的时间复杂度应分为建堆和向下调整;

建堆的时间复杂度:

计算总共向下交换的次数:

T(N)=1x(h-1)+2x(h-2)+........+2^(h-3)x2+2^(h-2)x1;

等比数列求和后

T(N)=2^h-h-1

由二叉树的性质得:N=2^h-1和h=log(N+1)

代入后为:T(N)=N-log(N+1)

用大O表示法:T(N)=O(N);

向下调整算法的时间复杂度:

使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。

所以最终堆排序的时间复杂度为O(N*logN),空间复杂度为O(1)。

快速排序

快速排序是公认的排序算法之最,是Hoare在1926年提出的一种二叉树结构的交换排序算法,其基本思想是:

        任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

递归实现

Hoare法

其步骤为:

  1. 选出一个基准值key,一般是最左边或最右边;
  2. 定义一个L和一个R,L找比基准值大的值,找到时停下,R找比基准值小的值,找到时也停下,都找到了后两者交换,然后继续找下去;
  3. 直到L和R相遇后,将相遇点的值与基准值key交换即可;
  4.  因为我们先定义基准值key是最左边,那么我们就要让R先走,不管基准值在左还是右,只要从它的对面开始找起,算法就是正确的。

为什么基准值在左边,就要让R先走呢?

答案:

        hoare法要求必须先从右边开始走起(基准值是left),因为每一次快排要求的是基准数的左边小于基准数,基准数的右边大于基准数;

如果从左边先走起,left停下来的原因有二:

        1.与right重合了

        2.找到比基准数大的数

        那么此时再走右边,倘若还未重合,right也会受到left<right的限制,可能会找不到小于基准数的值,那么重合时将left和keyi所指向的值进行交换,就可能发生错误。

 如图所示,如果让左边先走起(基准值是left),那就会导致交换后排序错误,可以自己尝试一下。

代码如下:

//Hoare法
int partSort1(int* a, int left, int right)//hoare法
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}
void QuickSort1(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = partSort2(a,begin,end);
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);

}

时间复杂度:O(NlogN)

挖坑法

 挖坑法单趟基本步骤如下:

  • 1.将key的位置(即为第一个元素的位置)作为第一个坑位,将key的值一直保存在变量key中。
  • 2.定义一个right从数组最后一个元素开始即为数组右边开始向左遍历,如果找到比key小的值,right停下来,将right下标访问的元素赋值到上一个坑位,并将right作为新的坑位。
  • 3.定义一个left从数组第一个元素开始即为数组左边开始向右遍历,如果找到比key大的值,left停下来,将left下标访问的元素赋值到上一个坑位,并将left作为新的坑位。
  • 4.当right和left相遇时,此时它们访问的元素绝对是坑位,只需将key里保存的key值放入坑位即可。
  • 5.让他们相遇位置的左区间和右区间同样执行上述四步(即为递归)。

代码如下:

//挖坑法
void QuickSort2(int* a, int left,int right)
{
	if (begin >= end)
		{return;}
    int key = a[left];
	int pit = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[pit] = a[right];
		pit = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[pit] = a[left];
		pit = left;
		
	}
	a[pit] = key;

    QuickSort2(a, left, pit - 1);//key的左序列进行此操作
	QuickSort2(a, pit + 1, end);
}
  

时间复杂度为:O(NlogN)

前后指针法

步骤如下:

  1. 定义一个key值,一般是最左或最右值
  2. 定义前后指针,前指针(cur)等于后指针(prev)+1,前指针用来找寻比key值大的数据,找到这个值后先将后指针+1,判断是否与前指针重合,如果重合则不交换,如果不重合则交换;
  3. 直到前指针越界了,终止这个循环;
  4. 终止循环后,key值到后指针的数(包括后指针)都是比key小的,后指针到前指针的值(不包括后指针,包括前指针)都是比key大的,将后指针的数与key值交换;
  5. 将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。 

代码如下:

//快速排序(前后指针法)
void QuickSort3(int* a, int begin, int end)
{
	if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
		return;

	//三数取中
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[midIndex]);

	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)//当cur未越界时继续
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	int meeti = prev;//cur越界时,prev的位置
	Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容

	QuickSort3(a, begin, meeti - 1);//key的左序列进行此操作
	QuickSort3(a, meeti + 1, end);//key的右序列进行此操作
}

时间复杂度为:O(NlogN) 

非递归实现

        通过人为创造一个栈来存放每次需要进行排序的区段的下标,进而模拟每次递归的操作,也可以使用队列,但要分清栈的后进先出和队列的先进先出。(这里我使用的是栈)

        为了方便使用,我们可以将快速排序的三种递归方法的单趟排序单独封装,然后再写一个非递归的函数,再在这函数里面调用我们三种递归方法的其中一种即可。

Hoare法单趟排序:

int partSort1(int* a, int left, int right)//hoare法
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

挖坑法单趟排序:

int partSort2(int* a, int left,int right)
{
	int key = a[left];
	int pit = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[pit] = a[right];
		pit = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[pit] = a[left];
		pit = left;
		
	}

	a[pit] = key;
	return	pit;
}

前后指针法单趟排序:

int partSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	int meeti = prev;
	Swap(&a[keyi], &a[meeti]);
	return meeti;
}

快速排序的非递归思想如下:

  1. 先创立一个栈来存放需要排序的序列的前后下标L和R;
  2. 需要通过先排左序列还是右序列决定先压L入栈还是压R入栈;
  3.  当栈不为空时,一次提取出两个下标,即需要排序的序列前后下标,排好序后又将该序列分为左右序列再次压栈,只到序列不再需要排序(序列的只有一个数据或者不存在)就不用再压栈;
  4. 重复以上操作,直到栈为空停止。

代码如下:

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

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);//先压尾,再压首,提取时就能先提取首后提取尾
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int keyi = partSort1(a, left, right);
		// [left, keyi-1] keyi [keyi+1, right]这就是需要排序的左右序列
		if (keyi + 1 < right)//判断是否还需压入栈
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}

		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
	}
	StackDestroy(&st);
}

 时间复杂度为:O(NlogN)

快速排序的两个优化

三数取中:

由上可知,快速排序的时间复杂度为O(NlogN),但这只是理想情况下的时间复杂度,即每次排序key都在序列的正中间,左右序列长度相等

每趟排序后key值都是序列的中间值,此时快速排序的时间复杂度才为O(NlogN),那我们怎么可能保证每次排序key值都能在中间值呢,显然这是不可能的。

那如果要考虑到最坏情况的话,我们的快速排序的时间复杂度甚至会退化成O(N^2)

比如我们是要一个升序序列,如果给你的数据是一个降序序列呢,key值是不是一直都是在最左边

T(N)=1+2+3+.......+N=(1+N)*N/2

大O表示法为O(N^2)。

其实快速排序的效率就取决于它每趟排序key值所处的位置,越靠近中间,则快速排序的效率就会越高,这就三数取中优化的由来。

代码如下:

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

用该函数的返回值来作为单趟排序的key值

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

小区间优化

        我们可以看到,就算是在最优情况下,每一层都会以2倍的形式进行递归次数的增加,如果数据量特别庞大时,最下面几层递归将要开辟的栈空间会特别庞大,并且相较于其他排序方法可能略显麻烦,那我们是不是可以设定一个判断语句,当递归到什么程度,剩下的数据我们采用更加高效的排序方法呢? 

        这就是小区间优化的由来。

代码如下:

void QuickSort0(int* a, int begin, int end)
{
	if (begin >= end)
		{return;}

	if (end - begin + 1 > 20)//可自行调整
	{
		//可调用快速排序的单趟排序三种中的任意一种
		//int keyi = partSort1(a, begin, end);
		//int keyi = partSort2(a, begin, end);
		int keyi = partSort3(a, begin, end);
		QuickSort0(a, begin, keyi - 1);
		QuickSort0(a, keyi + 1, end);
	}
	else
	{
		ShellSort(a + begin, end - begin + 1);//当序列长度小于等于20时,使用希尔排序
	}
}

归并排序

递归版本

         归并排序采用了分治法,它的基本思想是:先将两个子序列分别排好序,然后再将两者合并,从而得到一个完全有序的序列。

        ​​​​​​​

归并排序需要分成分解和合并两个步骤

分解:将序列不断分成两个不一定完全相等的子序列,直到序列无法继续分解为止;

合并:将子序列按顺序放进一个自创数组中排好序,再重新拷贝覆盖子序列。

代码如下:

void _MergeSort(int* a, int begin, int end, int* tmp)//功能函数
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	int begin1 = begin;
	int begin2 = mid + 1;
	int i = begin;
	while (begin1 <= mid && begin2 <= end)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= mid)//这下面两个while循环这里是为了防止两边奇偶的差别而导致最大的数据
	{                    //被漏掉了但是这里不知道哪边是奇哪边是偶,所以都判断一下
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end)
	{
		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);

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

	free(tmp);
}

时间复杂度为O(NlogN),空间复杂度为O(N)。

非递归版本

break法

        非递归实现不需要借助栈来完成,我们只需要控制每次需要合并的元素个数即可,使得序列最终变得有序。​​​​​​​

        非递归实现的难点在于需要不断控制边界,防止越界访问数组。

情况一:当数组的个数为奇数个;

情况二:当数组的个数为偶数个当不为2^N时;

以上两种情况都有越界访问的情况,我们需要对越界访问进行约束。

越界的的情况有三种:

  1. 第一组的end1越界
  2. 第二组的begin2,end2全部越界
  3. 第二组的end2越界

前两种情况我们可以直接对这一组归并直接break,退出当次循环,就不会导致越界;

第三种情况我们可以修正end2,使end2指向真实的尾值,也就是n-1。

最终代码如下:

void MergeSortNonr1(int* a, int n)//break法
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i = i + 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			printf("[%d][%d],[%d][%d] ", begin1, end1, begin2, end2);
			int j = i;
			if (end1 >= n)
			{
				break;
			}
			if (begin2 >= n)
			{
				break;
			}
			if (end2 >=n)
			{
				end2 = n - 1;
			}
			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));
		}
		printf("\n");
		gap = gap * 2;
	}
	printf("\n");
	free(tmp);
}

        当数据不是2的次方时,在前几轮排序中后面几个数是不会参与排序的,都被break掉了,直到它们全部在begin2-end2之间,这时end2一定会越界,那么就修正end2为n-1,直到这时,最后几个数才真正的参与了排序过程!! 

        同时要注意,这种方法需要每合并一小段就将该小段拷贝覆盖至原数组的数据,不能整段拷贝覆盖,不然会导致数据的丢失。

memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));

全部修正法

该方法和break区别在于对于三种越界的情况,全部采用修正的方法。

  1. 第一组的end1越界:那就将end1改为n-1,并且让第二组不进入while循环
  2. 第二组的begin2,end2全部越界:让第二组不进入while循环
  3. 第二组的end2越界:修改end2为n-1

代码如下:

void MergeSortNonr2(int* a, int n)//全部修正法
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i = i + 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			printf("[%d][%d],[%d][%d] ", begin1, end1, begin2, end2);
			int j = i;
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			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++];
			}
		}
		gap = gap * 2;
		memcpy(a , tmp , sizeof(int) * (n));
	}
	printf("\n");
	free(tmp);
}

时间复杂度为O(NlogN),空间复杂度为O(N)。

计数排序

        计数排序,又叫非比较排序。该算法不是通过比较数据的大小来进行排序的,而是通过统计数组中相同元素出现的次数,然后通过统计的结果将序列回收到原来的序列中。

        

        上列中的计数方法叫做绝对映射,即arr中的元素是几,就在count中对应的下标位置++,但这样会造成空间浪费,假设有一个[1005,1000,1008]的数组,那是不是要开辟一个能存放1009个元素的数组?

        所以我们要使用一个叫相对映射的方法,简单来说,就是将最小值作为0下标,其他数据的下标就是与这个最小值的差值,比如[1005,1000,1008],1008的下标应该就是8,最后计数环节就是计min+i出现的值。

总结:
 绝对映射:count数组中下标为i的位置记录的是arr数组中数字i出现的次数。
 相对映射:count数组中下标为i的位置记录的是arr数组中数字min+i出现的次数。

        注意的是,计数排序只能用于数据较集中的序列,不能用于数据过于分散的序列,否则会造成空间的浪费,并且计数排序只适用于整型,不能用于浮点型。

代码如下:

//计数排序
void CountSort(int* a, int n)
{
	int min = a[0];//记录数组中的最小值
	int max = a[0];//记录数组中的最大值
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	int range = max - min + 1;//min和max之间的自然数个数(包括min和max本身)
	int* count = (int*)calloc(range, sizeof(int));//开辟可储存range个整型的内存空间,并将内存空间置0
	if (count == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//统计相同元素出现次数(相对映射)
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	int i = 0;
	//根据统计结果将序列回收到原来的序列中
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
	free(count);//释放空间
}

时间复杂度为:O(N+range),空间复杂度为O(range)。 

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LLhaibao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值