常见八大排序

常见的排序八大排序分为四种类型,分别是插入排序、选择排序、交换排序,归并排序。其中插入排序包括直接插入排序和希尔排序,选择排序包括选择排序和堆排序,交换排序包括冒泡排序和快速排序,归并排序和剩余的计数排序自己单独作为一种类型,以下的排序都默认是排升序。


插入排序

直接插入排序

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定
void InsertSort(int* a, int sz)
{
	int begin = 1;//从第二个数开始插入排序,只有一个数就不需要排序
	while (begin < sz)
	{
		int end = begin;//记录要插入数的下标
		int temp = a[end];//记录要插入的数大小
		while (end >= 0)
		{
			if (temp < a[end - 1])
			{
				a[end] = a[end - 1];//要插入的数小,将前一个数后移
				end--;
			}
			else//大于前一个数,插入当前位置
			{
				a[end] = temp;
				break;
			}
		}
		    begin++;//后移一位数
	}
}

顾名思义,直接插入排序是一种比较直接的排序方法,它通过从数据之间的大小比较,找到数据的插入位置,然后完成部分的有序。插入排序是从第二个数开始排序的,通过与前面的数进行比较,找到插入的位置,完成了前面两个数据的有序之后,继续将下一个数进行插入排序,直到最后一个数完成插入排序,这样整个数据就变为了一个升序的数据。在插入的过程中,我们先保存要插入的数据,然后将要插入的数不断与前面的数进行比较,如果是比要插入的数大的话将该数往后移一位,这样直到遇到小的数就可以直接将数插入对应的位置。


希尔排序

  • 时间复杂度:不固定,O(N*logN)~O( N^2 )
  • 空间复杂度:O(1)
  • 稳定性:不稳定
void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

void ShellSort(int* a, int sz)
{
	int gap = sz;
	while (gap!=1)
	{
		gap = gap / 3 + 1;//防止最后一次gap不为1
		for (int i = 0; i < sz-gap; i++)
		{
			int end = i;
			while (end + gap < sz )
			{
				if (a[end] > a[end + gap])
				{
					Swap(&a[end + gap], &a[end]);
				}
				end += gap;
			}
			
		}
	}
}

希尔排序是在直接排序之后出现的一个排序算法,相比与直接插入排序,希尔排序对其作出了一些优化,在直接插入排序之间,先要进行预排序,这使得算法的效率更高了。与直接插入排序不同,希尔排序设置了一个间距gap,是将数据划分为多组数据,然后每组之间比较大小,将小的数据交换到前面,大的数据交换到后面,随着所有组交换完成,这样就完成了第一趟循环,接着改变每组数据之间的间距gap,使得gap的值变小,在下一趟循环中,进行更加精确的排序,这样做的目的是,做预排序,使得大部分小的数据都到数据的前半部分,而大的数据往后半部分,使得插入排序的效率更加高效,随着每一趟预排序结束,最后一趟序排序的间距gap变为1,此时就变成了我们所熟悉的直接插入排序,此时数据与不做预排序处理的数据相比,会更加的有序,在插入排序中会更加的高效,在某一个数寻找插入位置时会快很多,这就是希尔排序。


选择排序

选择排序

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1) 
  • 稳定性:不稳定
void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
void SelectSort(int* a, int sz)
{
	int begin = 0;
	int end = sz - 1;
	while (begin < end)
	{
		int maxi = begin;
		int mini = begin;
		for (int i = begin+1; i <= end; i++)//找到最大数及最小数的下标
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		//将最大值交换至最右边,最小值交换至最左边
		Swap(&a[begin], &a[mini]);
		if (maxi == begin)//当最左边为最大值时,记录最大值交换值后的下标
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		//范围缩小
		begin++;
		end--;
	}
}

选择排序,是一种不断筛选数据的过程,通过遍历数据,找到数据中最大数或最小的数,每次遍历都可以确定一个数的位置,然后缩小要筛选数据的个数。这里为了更高效一点,可以在遍历数据的时候同时将数据中最大及最小的数找到并分别交换到最左边和最右边,这样,每一趟循环遍历数据都找到对应最大最小两个值,并交换到对应的位置,最后完成对数据排序的升序。需要注意的是,在遍历数据的时候,保存的是最大最小数所对应的下标,所以在最后交换数据的时候可能会出现最大或最小值在边界,然后被换走的情况,这里需要我们格外注意。


堆排序

  •  时间复杂度:O(N*logN)
  •  空间复杂度:O(1)
  •  稳定性:不稳定
void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

void AdjustDown(int* a, int n,int sz)
{
	int parent = n;//向下调整起始位置
	int child = parent * 2 + 1;
	while (child < sz)
	{
		if (child + 1 < sz && a[child] < a[child + 1])//右子节点存在,确定较大子结点下标
		{
			child++;
		}
		if (a[child] > a[parent])//将更大的数据往上转移
		{
			Swap(&a[child], &a[parent]);
		}
		parent = child;
		child = parent * 2 + 1;
	}
}

void HeapSort(int* a, int sz)//排升序,建大堆
{
	int pos = sz - 1 - 1 / 2 + 1;//得到最后一个非叶子结点下标
	while (pos>=0)//建堆
	{
		AdjustDown(a, pos, sz);
		pos--;
	}
	//将堆顶数据放在最后,重新调整堆
	while (sz > 1)
	{
		Swap(&a[0], &a[sz - 1]);
		sz--;
		AdjustDown(a, 0, sz);
	}
}

顾名思义,堆排序是借助堆来进行数据的排序的,前面关于堆的介绍也介绍了堆排序,这里再详细介绍一下。堆作为一个特殊的结构,其两种形态,能被很好的运用在排序上,对于排升序来说,我们首先需要建立一个大堆,降序则建小堆,以升序为例,我们可以将数组中的数据视为一个堆,从第一个非叶子结点开始向下调整建大堆,不断往上,直到堆顶,也就是数据中第一个元素,建成一个大堆,此时,我们将堆顶的数与最后的叶子结点的数,也就是数组中最后一个数,两者之间进行交换,完成将最大的数置于最后,将数组的访问空间减少一个单位,接着我们可以重新从堆顶开始调整堆,再将堆顶数据与最后数进行交换,不断循环如此,直到堆能对数组的访问空间只为一个数单位时,此时该数为最小的数,完成对数据的排序。


交换排序

冒泡排序

  •  时间复杂度:O(N^2) 
  •  空间复杂度:O(1)
  •  稳定性:稳定(太稳了)

冒泡排序最为我们最早接触的一个排序算法,在排序算法中表现着实让我们失望,它在实际应用中没有出手的机会,但作为新手接触的第一个排序算法,确实十分具有教学意义的。

void BubbleSort(int* a, int sz)
{
	int flag = 0;
	for (int i = 0; i < sz - 1; i++)//冒泡sz-1趟
	{
		for (int j = 0; j < sz - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				flag = 1;
				int temp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = temp;
			}
		}
		if (flag = 0)//未发生交换说明数据已经有序
		{
			break;
		}
	}
}

冒泡排序是从第一个数开始,通过将两两数据之间进行比较,将大的数换到后面,小的数换到前面,直到最后一组数,完成将最大数交换到最后一个位置,每一趟循环都将剩余数中最大那一个交换到最后一个位置,循环如此,就完成了对数据排序的升序。


快速排序

  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(logN)
  • 稳定性:不稳定

快速排序是一个十分重要的排序算法,其版本也有三个,它是由Hoare于1962年提出的一种二叉树结构的交换排序方法,所以快速排序的最原始的版本就是Hoare版本的排序算法,而由于这种排序算法不是特别好理解,后面有人又依次开发出了两个版本的快速排序,分别是,挖坑法以及前后指针法,但其算法的效率没有任何改变,只是有了不同的理解,这三个版本我们都会介绍。

Hoare版
void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

void QuickSort1(int* a, int left, int right)
{
	if (left >= right)//区间不符,结束递归
	{
		return;
	}
    int begin = left;//记录两端下标
	int end = right;
	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[begin], &a[left]);
	keyi = left;
	QuickSort1(a, begin, keyi - 1);//左序列递归
	QuickSort1(a, keyi + 1, end);//右序列递归
}

Hoare版本的快速排序思想是任取待排序元素序列中的某元素作为基准值(这里默认采用第一个元素作为基准值),按照该排序码将待排序集合分割成两子序列,先从右侧开始找到小于基准值的数,然后再从左侧找到大于基准值的数,将两数位置互换,不断重复,知道两左右两侧相遇,最后再将基准值与相遇位置的数进行交换,这样的得到的数据就会有左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,每一次调用函数都将有一个基准值被放在正确的位置,并划分新的左右序列,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

挖坑法
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)//区间不符,结束递归
	{
		return;
	}
	int hole = left;//记录坑位下标
	int begin = left;//记录两端位置
	int end = right;
	int key = a[left];//记录第一个坑位的值
	while (left < right)
	{
		while (left < right && a[right] >= a[hole])
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;//新的坑位
		while (left < right && a[left] <= a[hole])
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;//新的坑位
	}
	a[hole] = key;
	QuickSort1(a, begin, hole - 1);//左序列递归
	QuickSort1(a, hole+1, end);//右序列递归
}

挖坑法版的快速排序是将基准值作为一个坑位,先从序列右侧开始找比基准值小的数,然后将该数填入坑位,使得该数原本位置成为新的坑位,再从左侧开始找比基准值大的数,填入新的坑位,又使得这个数原本所在的位置成为新的坑位点,直到左右两侧相遇,最后再将记录好的第一个坑位的值填入最后的坑位,最后的基准值就被放在了正确的位置,这样就完成了一趟循环,划分好了左右两边的序列,再让左右两边的序列进行递归划分新的序列,这样不断重复,就可以得到一个升序的序列。


前后指针法
void QuickSort3(int* a, int left, int right)
{
	if (left >= right)//区间不符,结束递归
	{
		return;
	}
	int keyi = left;
	int prev = left;
	int cur = left+1;//cur找小,找到后prev+1交换值
	int begin = left;//记录两端位置
	int end = right;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])//找到小值
		{
			Swap(&a[++prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	QuickSort3(a, begin, prev-1);
	QuickSort3(a, prev + 1, end);
}

前后指针法版的快速排序,顾名思义是需要借助两个指针,其原理是使用数cur、prev,记录序列最左端数的下标,同样以左端数为基准值,让cur寻找比基准值小的数的下标并记录,当找到符合要求的数时,会先令prev记录的下标加上1,也就是prev所指向的下一个数与cur所指向的数进行交换,直到cur遍历完整个序列此次循环才结束,最后再让基准值与prev所指向的数进行交换,这样以此时prev所指向的基准值为分界线划分出左右两端序列,再让左右两端序列进行递归,这样在不断的递归中,让每个基准值被放在正确的位置,最后的到升序的序列。


非递归式的快速排序
typedef int STDataType;
typedef struct stack
{
	STDataType* a;
	int top;//栈顶
	int capacity;//栈容量
}ST;

void STInit(ST*ps)//栈的初始化
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

void STPush(ST* ps, STDataType x)//入栈
{
	assert(ps);
	if (ps->capacity == ps->top)//检查容量是否已满
	{
		int newCapacity = (ps->capacity==0 ? 4:2*ps->capacity);
		//STDataType* temp = (STDataType*)realloc(ps->a,sizeof(STDataType)*ps->capacity);
		STDataType* temp = (STDataType*)realloc(ps->a,sizeof(STDataType)*newCapacity);
		if (temp == NULL)
		{
			perror("realloc");
			exit(-1);
		}
		ps->a = temp;
		ps->capacity = newCapacity;
	}
	ps->a[ps->top] = x;
	(ps->top)++;
}

void STPop(ST* ps)//出栈
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}

STDataType STTop(ST* ps)//获取栈顶元素
{
	assert(ps);
	assert(STEmpty(ps)==false);
	return ps->a[ps->top - 1];
}

bool STEmpty(ST* ps)//判断栈是否为空
{
	assert(ps);
	return ps->top == 0;//top为0,返回true,不为0返回false
}

void STDestroy(ST* ps)//销毁栈
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;	
	ps->top = ps->capacity = 0;
}
void QuickSortNonR(int* a, int left, int right)
{
	if (left == right)//单个数,不用排序
	{
		return;
	}

    //将待排序数据两端区间下标录入栈中
	ST* st = (ST*)malloc(sizeof(ST));
	STInit(st);
	STPush(st,right);
	STPush(st,left);

	while (!STEmpty(st))
	{
		//取出要进行快排的两端下标
		int begin = STTop(st);
		STPop(st);
		int end = STTop(st);
		STPop(st);
	
		if (begin < end)//满足排序区间,前后指针法进行单趟快排
		{
			int cur = begin + 1;
			int prev = begin;
			while (cur <= end)
			{
				if (a[cur] < a[begin])//将cur与prev所指向下一个数交换
				{
					Swap(&a[cur], &a[++prev]);
				}
				cur++;
			}
			Swap(&a[prev], &a[begin]);//以下标prev为分界点,将两端区间数再进行排序

            //左右区间端点入栈
			STPush(st, prev - 1);
			STPush(st, begin);
			STPush(st, end);
			STPush(st, prev + 1);
		}
	}
	STDestroy(st);
	free(st);
}

 对于非递归式的快速排序,可以让我们更好的理解快速排序的过程,也算是多掌握一项技能,对于使用递归算法的快速排序,我们知道其思想是类似与二叉树的遍历,与二叉树前序遍历十分相似,快速排序每趟选取排好一个基准值的位置,接着将左右剩余序列划分为新的区间继续排序,不断如此,为了使用非递归的方式写出快速排序我们可以借助栈来实现这一过程,栈的先进后出的特性十分契合这一过程,我们定义一个简单的栈,先将待排序的数据的左右两端下标入栈,接着再从栈顶取出两个数据(取出一个即出栈一个元素),得到待排序的左右两端的下标,判断两下标是否满足需要排序的情况,如果满足,接下来进行正常一趟排序,这里三种快排方法任选其一,接着则是关键了,将以排序好的基准值的下标为界限所划分出来的两段序列的左右下边入栈,以便下一趟的排序,而循环继续的条件为是栈不为空,当栈为空时,代表排序完成,最后再将栈开辟的空间释放即可。


快速排序的改良
三数取中选取合适的基准值

基准值的选取堆与快速排序的效率有着比较大的影响,理想情况下,每次由基准值所划分的左右两个序列的区间相当是最好的,这样就相当与一颗左右子树高度相当的情况,此时的快速排序的效率,也就是理想情况下的时间复杂度为 O(N*logN),但是正常情况下并不能达到这样的效果,而是会出现某一区间数多,另一区间数少的情况,甚至极端情况下,当要排序的数的序列恰好为升序时,以此时的基准值所划分的左右序列,其左序列不存在,也就是说,剩余的数都将被划分在右序列中,此时递归的深度将被加大到最大,算法效率变得最差,时间复杂度为 O(N^2),为了避免出现这种情况,三位取中就有着其存在的意义,我们需要尽量将基准值变为一个中等数的大小,理想情况下是全部数据的中位数,虽然我们并不能达到理想情况,但是可以通过这种方式来提高算法的效率,避免出现上面所说的最坏的情况。

int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] < a[left])
	{
		if (a[left] < a[right])//right最大
		{
			return left;
		}
		else if (a[mid] > a[right])//right最小
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
	else//mid>left
	{
		if (a[mid] < a[right])//right最大
		{
			return mid;
		}
		else if(a[left]>a[right])//right最小
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
减少递归子区间

随着递归的深入,被划分出来进行递归的左右序列的长度逐渐减小,此时可以采用非递归的形式将此时的序列进行排序,对于此时的少部分的数的排序,我们可以采用插入排序的方法来排序,如此一来可以减小递归的深度,对快速排序做出些许优化,提升快速排序算法的效率。在进行是否递归左右序列时,我们先对左右序列区间大小进行判断,如果区间较大,待排序的数较多时则进行递归排序,否则直接使用插入排序进行排序该起区间的数。对于Hoare版本的快速排序被优化后:

void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int mid = GetMidi(a, left, right);//三数取中
	Swap(&a[left], &a[mid]);//将基准值换到左边
    int begin = left;//记录两端下标
	int end = right;
	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[begin], &a[left]);
	keyi = left;//
	//左右两侧
	if (keyi-1 > begin + 9)//剩余的数个数大于10个进行递归
	{
		QuickSort1(a, begin, keyi - 1);
	}
	else
	{
		InsertSort(a, begin, keyi - 1);//插入排序
	}
	if (end > keyi + 1 + 9)
	{
		QuickSort1(a, keyi + 1, end);
	}
	else
	{
		InsertSort(a, keyi, end);
	}
}

三路划分——针对有大量重复数据的情况

有时快速排序中可能会出现对有大量重复数据进行排序的需求,使用上面常规的方法无法对其做出效率上的优化,这时三路划分则可以完美解决这种极端的情况,当数据全为相同时甚至只需要O(N)的时间复杂就可以解决此类问题,但是以上面的优化过的方法仍然需要进行大量递归对数据进行相对应的排序。

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int mid = GetMid(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = a[left];
	int begin = left;//记录两端下标
	int end = right;
	int cur = left+1;
	while (cur <= right)//三路划分
	{
		if (a[cur] < key)
		{
			Swap(&a[cur++], &a[left++]);
		}
		else if (a[cur] == key)
		{
			cur++;
		}
		else
		{
			Swap(&a[cur], &a[right--]);
		}
	}
	//小:[begin][left-1],等:[left][right],大:[right+1][end]
	QuickSort(a, begin, left-1);
	QuickSort(a, right+1, end);
}

三路划分顾名思义是将数据划分为三个区间,这三个区间分别为小于基准值、等于基准值、大于基准值的三种情况,对于等于基准值的区间则不进行递归展开,而是将左右两端区间递归展开,这里也可以配合,三数取中和减少递归子区间的方法使用进一步优化,这里只使用了三数取中的。其具体思路是通过一个指针cur遍历从头遍历整个数据,左右两端也分别用两个指针left、right指向两端数据,当cur遇到比基准值小的就将此时的数据与左端left所指向的数据进行交互,然后left和cur同时指向下一个数据,当遇到与基准值相同的数据时,cur跳过一个单位指向下一个数据,而当遇到大于cur的数据时,将cur所指向的数据与右端right所指向的数据进行交换,只让right减一,指向前一个数据,继续进行下一个循环,判断此时cur所指向数据的大小情况,直到cur所指向的数据超过了right循环则结束。此时完成对数据的划分,然后只需要对左右两端区间序列的数进行递归排序即可。


归并排序

  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(N)
  • 稳定性:稳定

归并排序单独作为一种排序,与前面其它6种排序有所不同,归并排序的空间消耗比较大排序N个数据时需要开辟等同大小的空间。归并排序是一种将已经有序的子序列之间两两相互合并得到新的有序序列的一种排序。

void _MergeSort(int* a, int left, int right, int* temp)
{
	if (left >= right)//不满足递归区间
	{
		return;
	}
	int mid = (left + right) / 2;//找到中间值下标

	_MergeSort(a, left, mid, temp);
	_MergeSort(a, mid + 1, right, temp);

	//归并
	int begin1 = left, begin2 = mid + 1;
	int end1 = mid, end2 = right;
	int cur = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[cur++] = a[begin1++];
		}
		else
		{
			temp[cur++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		temp[cur++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[cur++] = a[begin2++];
	}

	memcpy(a+left, temp+left, sizeof(int) * (right - left + 1));//将数据拷贝回原数组
}

void MergeSort(int* a, int left, int right)
{
	int* temp = (int*)malloc(sizeof(int) * (right - left + 1));
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, left, right, temp);
	free(temp);
}

归并排序由于需要开辟待排序数据相同额外新的空间,所以采用两个函数来实现,一个是用于开辟空间,掉用另一个具体实现的函数,归并排序首先会将带排序的数据划分为左右两个相当的区间,通过这方式传递左右两个序列的参数,不断递归调用,知道无法满足递归的区间,这样的目的是为了从最小的两组进行归并,得到一个新的有序的序列,在递归的最深层,先是由两数之间归并排序,的到新的有序的一组含有两个数据的序列,然后在返回上一层的递归调用,进行归并得到含有四个数据的有序序列,这样,递归不断往上一层返回的过程,归并两组有序序列,在得到一个新的有序序列,如此反复,直到第一层的函数调用,被划分的左右两区间的数已经变为两组有序的数据,最终对左右两组数据进行归并得到最终有序的序列数据。在将在储存在已经开辟好的空间里的排好序的数据拷贝回原来的数组,就完成了对数据的排序。

非递归的归并排序

为了 更好的了解归并排序,还需要了解非递归式的归并排序。

void MergeSortNonR(int* a, int sz)
{
	int* temp = (int*)malloc(sizeof(int) * sz);
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	int right = sz - 1;
	while (gap < right)
	{
		for (int i = 0; i <= right; i += 2 * gap)
		{
			int begin1 = i, begin2 = i + gap;
			int end1 = begin1 + gap - 1;
			int end2 = begin2 + gap - 1;
			int cur = begin1;

            //begin1一定不越界,end1越界或begin2越界结束并归
			if (begin2 > right)//右半区间左侧越界
			{
				break;
			}

            //end2越界需要改变并归右区间
			if (end2 > right)//右半区间右侧越界
			{
				end2 = right;
			}
			
			while (begin1 <= end1 && begin2 <= end2)//归并
			{
				if (a[begin1] <= a[begin2])
				{
					temp[cur++] = a[begin1++];
				}
				else
				{
					temp[cur++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[cur++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[cur++] = a[begin2++];
			}
            //将并归后的数据拷贝回原数组
			memcpy(a + i, temp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
		printf("\n");
	}
	
	free(temp);
}

非递归式的归并排序的基本思想是起始通过设置一个gap间距,gap为每组归并数据的个数,起始时的gap为1,相当与递归式归并的最深层的函数调用,此时是将相邻两个数归并为一个含两个数的有序序列,完成每组的归并之后将gao值扩大两倍,因为每次循环的归并将会的到一个两倍大小的有序数序列,不断如此,最终我们同样能够得到一个有序序列的数据。在此之间我们需要注意的是,在进行分组归并的时候可能会出现越界的情况,我们只能确保两组数据的左区间数据的左端begin1一定不越界,但是剩余的end1、begin2、end2我们保证不越界,此时我们就需要分情况考虑,对于越界的情况,可能分为了三种,一种是左区间右端end1越界,此时右区间一定越界,剩余一个不完整的左区间,不需要进行归并排序,第二种情况是右区间的左端begin2越界,此时同样是只有一个左区间,不需要进行归并排序,所以在讨论情况时,我们可以将这两种情况归为一类,即不需要进行归并排序,第三种情况则是右区间的右端end2越界, 此时左右区间都有数据,需要进行归并排序,但是需要将右区间的右端改为该数据的最右端,防止越界发生,将右区间剩余不越界的数与左区间进行归并排序。此外,每次趟归并好的数据需要拷贝回原数组,否则,对于下一趟循环的数据归并排序时,数据就没有发生改变,仍然是原数据。

计数排序

时间复杂度:O(MAX(N,范围)),与数据大小范围有关。
空间复杂度:O(范围)
稳定性:稳定

计数排序与其它七种排序都有所不同,其他排序归根结底是通过比较数据之间的大小来进行排序的,当要排序的数据的范围比较集中时,其算法效率极其高,但是对于数据范围波动很大的数据排序算法效率比较低,其适用的场景有限,但在实际运用中排序仍然有计数算法的一席之地。

void CountSort(int* a, int sz)
{
	if (sz == 1)//单个数不排
	{
		return;
	}

	int min, max;// 记录最值
	min = max = a[0];
	for (int i = 1; i < sz; i++)//找到最值
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}

    //开辟对应数据范围个空间,将数据全置0(由calloc开辟的空间中的数据会被初始化为0)
    int* count = (int*)calloc(max - min + 1,sizeof(int));

	for (int i = 0; i < sz; i++)//计数
	{
		count[a[i] - min]++;
	}

	int j = 0;
	for (int i = 0; i < max - min + 1; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

计数排序的实现相对于前面几种排序显得更加简单,它是一个需要开辟空间来统计数据中每个数据的个数的算法,为了尽量减少空间的消耗,计数排序首先要遍历一遍原数据,找到数据的最值,以便开辟对应个范围个大小的空间,将已经开辟好空间的数据全置为0,一遍对数据的统计,这里使用calloc函数可以很好的解决,calloc函数在开辟空间的同时,会将其中的数据初识化为0,之后需要再次遍历原数据,统计各个数据出现的次数,原数据中每个数据都对应一个数组的下标表,用于统计数据出现的次数,从原数据的最小值开始,被统计的数的数组下标为0,然后依次+1,这里只需要将原数据的数据减去最小值即可以得到每个数对应的下标值,直接统计对应数据的出现次数,最后再直接将已经统计好的数据,覆盖回原数据即可得到一个有序的序列。

排序的稳定性

排序的稳定性是一排序算法中常常讨论到的一个话题,那么稳定性到底是什么呢,在我自己还没有了解过排序算法稳定性的时候,我听到这个词判断稳定性为一个算法的适用场景?泛用性强的强的稳定?实则不是如此,排序算法的稳定性指的是排序前后各个数据的相对位置是否会发生改变,如果不会发生改变则代表该排序算法是一个稳定的,反之则不稳定。对于排序算法的稳定性我们不应靠强行记忆,而是通过理解每个排序算法的过程,找到能够举出反例会使得排序算法数据排序前后相对位置发生变化的场景。


结语:

本期关于二叉树链式结构的相关知识到这就介绍完了,又是一篇长篇大论,干货满满,如果感觉对你有帮助的话还请点个赞支持一下!有不对或者需要改正的地方还请指正,感谢各位的观看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
数据结构中的八大排序算法,是指常见的八种用于对数据进行排序的算法。这八种算法分别是冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序和基数排序。 冒泡排序是一种简单的排序算法,通过不断比较和交换相邻元素的位置,使得最大(或最小)的元素逐渐往后(或往前)移动。 选择排序是一种简单直观的排序算法,每次选择未排序序列中最小(或最大)的元素,放到已排序序列的末尾。 插入排序是一种简单直观的排序算法,将一个待排序的元素插入到已部分排序的数列中的合适位置。 希尔排序是一种改进的插入排序算法,通过将待排序数列分组,并对每个分组进行插入排序,然后逐渐减小分组规模,最后进行一次插入排序。 归并排序是一种分治思想的排序算法,将待排序数列不断分割成较小的数列,然后再将这些较小的数列按照顺序进行合并。 快速排序是一种分治思想的排序算法,通过选择一个中间的基准元素,将数列分割成两部分,然后分别对这两部分进行排序。 堆排序是一种利用堆这种数据结构排序算法,通过将待排序数列构建成一个大(或小)顶堆,然后逐步将堆顶元素与最后一个元素交换,并调整堆结构。 计数排序是一种非比较型的排序算法,通过统计待排序数列中每个元素出现的次数,然后依次输出即可。 基数排序是一种非比较型的排序算法,通过对待排序数列的每个位进行排序,依次从低位到高位进行。 这里简单介绍了八大排序算法的基本思想和实现方法。在实际应用中,不同的排序算法适用于不同的场景和要求,我们需要根据具体情况选择合适的算法

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值