常见的排序算法包括插入、希尔、选择、堆、快速、归并排序等

一.插入排序

1.插入排序基本思想

        插入排序的基本思想就是通过构建有序序列,对于未排序数据在已排序序列从后向前扫描找到相应的位置并插入。通俗一点的解释就是和打扑克摸牌类似。首先你手中的牌一定是有序的,这时候如果你新抓了一张牌,你想让其有序就得从前向后或者从后向前找到它合适的位置。

        而我们的插入排序采用的是从后向前查找新数据插入的位置 ,比如上面的图片,7要进入这个数组里面,从后向前查找,10比7大,那么就将10向后移动,7继续和前面的数据比较,5比7小那么就将7放在5的后面。

                                                                        1.1动图演示

2.单趟代码实现及解释 

        下面我们来完成单趟代码

首先我们定义已经排好序的数组a的末尾是end,我们要从头开始去排序(因为插入一个新数据必须要保证之前的数据是有序的,我们没有办法直接在数组的末尾进行操作) 。这样我们摸得第一个数字是4,4只有一个数前面没有数据所以他就是有序的,所以就让他来做第一趟排序的end;然后我们去摸第二个数据,将他放在tmp中。显然它比4大那么它就直接放在end后面即可。

        而后end++到下一个位置,然后我们再抓下一个数字1,将它放到tmp中,而后和end对比end=7,tmp=1,这时只要将a[end+1] = a[end]; 然后--end;就可以将7挪到1的位置并且将end前移为下一组4和1比较做准备。而4也大于1, 所以4被换到a[1]的位置,这时候end已经来到数组的-1。最后我们要将tmp的数组放在已经挪好的位置中。

        接下来我们根据上边的思路完成单趟插入的代码

    //这里使用i来控制end向后移动 i初始为0;
    int end = i;
	int tmp = a[i+1];
	while (end >= 0)//单趟插入循环条件
	{
		if (a[end] > tmp) //将比tmp大的数向后挪
		{
			a[end + 1] = a[end]; 
			end--;
		}
		else
		{
			break;
		}
	}
	a[end + 1] = tmp;//将要插入的数字放到相应位置

现在已经完成了单趟排序,我们只要控制end从前向后走即可完成排序;这样我们就完成了插入排序。如下是插入排序完整代码:

void InsertSort(int* a, int n)
{
	// [0,end] 有序 ,插入tmp依旧保持有序;
	// 从后向前比较  比不到了 比所有值都小走到-1停下;
	for (int i = 1; i < n; i++)
	{
		int end = i-1;
		int tmp = a[i];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	} 
}

二.希尔排序

1.希尔排序基本思想

        希尔排序是插入排序的一种,它也被称为缩小增量排序。通俗一点来说希尔排序的基本原理还是插入排序,希尔排序比插入排序多的一点就是它可以对数据进行预排序,使数据接近有序,最后在进行一遍插入排序,这样插入排序的效率会高出许多。因为当数据越有序时插入排序所需要挪动的数据就变少了很多,所以希尔排序比直接插入排序要快很多。

        希尔排序的主要思路是将数据等距分组,然后对不同的组进行插入排序,即分组插入排序。我们假设间隔gap = 3的数据分为一组。例如下图的分组:

图2.1 

如图我们将这组数据分成了三组也就是gap组,这些被分割的数组的数据分别为4986732145;而我们接下来需要对这些组数分别进行插入排序;首先我们要排序第一组数据,通过插入排序的逻辑我们很容易得到这一组数据变成了4689;而数组的整体序列就变为4716348259

                                                                            图2.2

按照这个逻辑我们将剩余两组数据通过插入排序排后整体的序列为:4216348759

                                                                         图2.3

这样就达到了我们想要的预排结果,大数被换到了后面,小数换到前面。 这是当gap=3时进行的预排序,当gap减小时我们会发现整个数组越有序当gap=1时,他就是我们上面的插入排序。下面用动态图片演示希尔排序的整个过程。

        当gap = 3时;因为gif只支持60s,最后一趟排序没有加上。但是最后一组是有序的。

                                                                        图2.4

        当gap = 2时;

        当gap = 1时;此时为插入排序。排完一趟数据就变得有序了。

2.单趟代码实现及解释

        根据上面我们的思想我们首先得定义一个gap来确定数据的间隔,也就是数据的总组数。这里我们以gap = 3来做代码实现。用变量end来确定数据的尾。用tmp来表示下一个要插入的数据。

        这里end是我们第一个数据,tmp是待插入数据,如此根据图示我们很轻易就可以写出单组的排序。

        int gap = 3;

	   for(int i = 0; i< n-gap;i+=gap)//控制end持续向后移动,这里n是数据总个数
	   {
		   int end = i;
		   int tmp = a[end + gap];
		   while (end >= 0)
		   {
			   if (a[end] > tmp)
			   {
				   a[end + gap] = a[end];
				   end -= gap;//找到end前面的数继续和tmp比较;
			   }
			   else
			   {
				   break;
			   }

		   }
		   a[end + gap] = tmp;
	   }

        这就是单组排序对应上面绿色的那一组end从零开始完成一组排序。再此基础上我们只要再加一组循环,将分出来的三组数据都进行排序即可完成对gap = 3 的排序了。代码如下。

       int gap = 3;

	   for (int k = 0; k < gap; k++)//一组一组排序
	   {
		   for (int i = k; i < n - gap; i += gap)
		   {
			   int end = i;
			   int tmp = a[end + gap];
			   while (end >= 0)
			   {
				   if (a[end] > tmp)
				   {
					   a[end + gap] = a[end];
					   end -= gap;//找到end前面的数继续和tmp比较;
				   }
				   else
				   {
					   break;
				   }

			   }
			   a[end + gap] = tmp;
		}
	   

以上为第一种多组排序方法,下面是第二种



    int gap = 3;

	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 -= gap;//找到end前面的数继续和tmp比较;
			}
			else
			{
				break;
			}

		}
		a[end + gap] = tmp;


	}

        这是gap = 3时对数组分组进行的排序我们画图发现,此次排序达到了预排序的效果。如图2.3及图2.4演示的一样。大数都被排到了后边小数跑到了前面。但是他还不是有序的,所以我们要调整gap的大小,直至gap==1时此时单组排序就变成了插入排序,经过此次排序即gap==1时数组变得有序。因此我们要逐渐调整gap的大小直至gap==1。---  所以我们要在此基础上再套一层循环来控制gap。

void ShellSort(int* a, int n)
{

       int gap = n;

	   while (gap > 0)
	   {
		   gap = gap / 3 + 1;///这里加一防止gap直接被整除变成0;

		   for (int k = 0; k < gap; k++)
		   {
			   for (int i = k; i < n - gap; i += gap)
			   {
				   int end = i;
				   int tmp = a[end + gap];
				   while (end >= 0)
				   {
					   if (a[end] > tmp)
					   {
						   a[end + gap] = a[end];
						   end -= gap;//找到end前面的数继续和tmp比较;
					   }
					   else
					   {
						   break;
					   }

				   }
				   a[end + gap] = tmp;
			   }
		   }

	   }
}

        以上就是希尔排序的函数实现即思路。 

三.选择排序

1.选择排序基本思想

        选择排序的基本思路是从头开始查找最小的数,然后让第一个数据和找到的最小数据交换,然后继续查找次小的数和第二个位置的数交换。如此往复直到数组有序。

                                                                        3.1(图片来自菜鸟教程)

2.选择排序代码实现

        我们实现的是查找两个数,上面的思路是从头查找小的数,这里增加了从后向前查找最大的数和最后面的数交换的逻辑。

        首先我们完成的是一趟排序的代码。这里我们用begin来指向数组的第一个数。end指向数组的第二个数。

       int begin = 0;
	   int end = n - 1;

	   int mini = begin, maxi = end;
	   for (int i = begin; i <= end; i++)//循环查找
	   {
		   if (a[i] < a[mini])
		   {
			   mini = i;
		   }
		   if (a[i] > a[maxi])
		   {
			   maxi = i;
		   }
	   }
	   Swap(&a[begin], &a[mini]);//这个函数用来交换数据
	   if (begin == maxi)///如果最大的数就是begin那么、begin就被换到mini的位置上了;所以要做交换
	   {
		   maxi = mini;
	   }
	   Swap(&a[end], &a[maxi]);

        接下来我们只要加一个循环即可完成选择排序。

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int maxi = begin, mini = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				maxi = i;
			}
		}
		Swap(&a[begin], &a[mini]);
		//如果maxi和begin重叠
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		++begin;
		--end;
	}	
}

四.堆排序

1.堆排序的基本思想

        如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆的性质: 堆中某个节点的值总是不大于或不小于其父节点的值; 堆总是一棵完全二叉树。

        通俗来讲堆是一颗完全二叉树,这里以小堆为例,它的特点是它的所有父节点都大于等于它的孩子节点。如图所示:

                                                                         图3.1

         堆的逻辑结构是一颗树,但是其物理结构是一个数组。父节点和子节点的关系为parent = (child-1)/2; 现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整 成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。所以我们从最后一个孩子节点来向下调整,因为一个叶子节点就可以看作是一个小堆。我们通过最后一个节点找到他的父节点,就可以对以这个父节点为根的树进行向下调整,因为它的孩子都是叶子节点。

        而利用堆来排序的原理是堆的顶永远是最大/最小的数。而将堆顶的数据挪到最后一个节点,此时我们就找到了最大的数,接下来我们只要控制让尾部的数据不参与这个堆,然后从最上面的根节点进行一次向下调整,那么就可以很快的找到次大的数据了。重复这个过程我们就可以得到一组升序序列。

                                                                图3.2(图片来源于菜鸟教程) 

2.堆排序代码实现

         首先我们要完成向下调整算法。我们用parent代表父亲节点,用child代表孩子节点。n表示数组数据个数。这里我们以建立一个大堆为例。首先我们先找到最后一个节点的父节点,我们需要比较次父节点和两个子节点之间的大小(子节点等于父节点*2+1),让后进行交换。然后找到下一个父节点在进行向下调整算法;下一个父节点就是前面父节点的下标大小减1。这样我们就可以将数组建成一个大堆了。

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)//向下调整结束条件
	{
		if ((child + 1 < n) && (a[child] < a[child + 1]))//这里找出大的child
		{
			child++;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);

			parent = child;
			child = parent * 2 + 1;
		}
		else//如果孩子比父亲小就不用再调整了
		{
			break;
		}
	}
}

        接下来我们就要建立大堆再进行排序了。

void HeapSort(int* a, int n)
{
    for(int i = (n-2)/2; i >= 0; i--)//向下调整建堆
    {
        AdjustDown(a, n, i);
    {
    
    int end = n-1;
    //循环交换堆顶和尾的数据,使数组变有序。
    while(end>0)
    {
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

五.快速排序

1.快速排序的递归实现hoare法

        快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

        总的来说快速排序是先找到基准值(我们在这里将基准值定义为key,key永远在最左边),然后依据此基准值将数据分为两半,我们以升序为例,那么此基准值的左边的数都是比key小的数,而右边都是比key大的数。接下来我们要继续持续这个过程,对key左边和右边的值再次进行快速排序。直到只剩下一个数据时停止。因为key的左边的数字比key小,右边的所有数都比key大,所以一次快速排序后基准值就到达了其最终位置

        hoare法代码实现基本思路,这里我们用left代表最左边的数据,right代表最右边的数据。首先right先走找到比key小的值,然后left向后走,找到比key大的值,接下来交换left和right的值right继续寻找比key小的值,重复这个过程直到right遇到left停止。当right与left相遇时交换left和key的值就完成了第一个大区间排序--(这里存在三种停止情况,第一种right没有找到比key小的值一直向前走直到和left相遇,而left在最左边,说明key就是最小的数。第二种right找到了比left小的数,left找到了比key大的数,left和right交换后,right继续走没有找到比key小的数和left相遇,这时left的数刚好比key小,交换left和key完成此次排序。第三种是第二种的相反,right找到了比key小的数,但是left没有找到比key大的数,left和right相遇直接交换left和key的数据即可完成此次排序)。此时的key的位置就是最终排序完成的位置最后我们要返回left,也就是key值所在的位置作为下次左右区间执行排序提供区间的值。动图演示如下:

                                                                        图5.1 

        接下来我们完成单趟快速排序的代码。

int ParkSort(int* a, int left, int right)
{

	int keyi = left;
	while (left < right)
	{

		while (left < right && a[right] >= a[keyi])//right找小
		{
			--right;
		}

		while (left < right && a[left] <= a[keyi])//left找大
		{
			++left;
		}
		Swap(&a[right], &a[left]);//交换
	}

	Swap(&a[keyi], &a[left]);
	return left;
}

        这样我们就完成了一次排序,将小于key的都挪到了它的左边,大于key的挪到了右边,接下来我们需要对0到key-1这个区间,和key+1到right这个区间分别进行排序,然后再次分割直到只剩下一个数据为止。代码如下:

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

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

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

图示如下:

 

                                                                               图5.2

2.快速排序的非递归实现使用栈 

        当递归的深度非常大时,递归情况下会导致栈溢出问题;所以这里我们实现一下快速排序的非递归方法。

        这里我们实现非递归是使用的栈(这里的栈是数据结构的栈,并不是计算机的物理结构);这里我们实现的具体思路是将left和right存入栈中,调用排序函数时我们先在堆中取到left和right的值,然后传到ParkSort函数中,当此次排序完成,返回了keyi,然后我们将left,keyi-1,right,keyi+1压入栈中,循环读取栈中区间的数据,即可模拟实现递归结构。这个前提需要我们有一个栈Stack。下面是代码实现:

void QuickSortNonR1(int* a, int begin, int end)
{
	Stack s;
	StackInit(&s);//初始化栈
	StackPush(&s, end);
	StackPush(&s, begin);
	while (!StackEmpty(&s))
	{
		int left = StackTop(&s);
		StackPop(&s);
		int right = StackTop(&s);
		StackPop(&s);

		int keyi = PartSort(a, left, right);

		if (keyi + 1 < right)
		{
			StackPush(&s, right);
			StackPush(&s, keyi + 1);
		}
		if (keyi - 1 > left)
		{
			StackPush(&s, keyi - 1);//循环上去后第二个读取的数据
			StackPush(&s, left);//循环上去后第一个读取的数据
		}
	}

    StackDestroy(&st);//销毁栈
}

        这里增加了一些对快速排序的简单优化,第一个优化为三数取中,使key的值尽可能排在数组中间。这里我们用的是伪随机取法。

//三数取中
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + (rand() % (right - left))) / 2;//注意rand的使用需要srand生成随机数种子
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}

	}
	else
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return mid;
		}
		else
		{
			return right;
		}

	}
	return left;
}

 

六.归并排序

1. 归并排序的递归版本实现

        归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

        归并排序将数据分割成两半,直至将数据切割为1个,然后合并两个有序区间。这里我们用mid来代表数据中间位置的下标,两个区间的开始和结束分别为,begin1,end1,begin2,end2; 我们将两个区间切割完后开始归并,归并的思路是在两个有序区间中(这里以升序为例),两个序列的到一个数比较,找到最小的数插入到另一个临时数组tmp中,然后再次比较插入。如果一个数组没有数据了,就将另一个数组剩下的数据拷到临时数组tmp中。最后将数据拷贝回最初的数组中。如下动图:

                                                                        图6.1

        接下来我们完成归并排序的代码。

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, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = 0;

	while (begin1 >= end1 && begin2 >= end2)
	{
		if (a[begin1] >= a[begin2])
		{
			tmp[i++] = a[begin2++];
		}
		else
		{
			tmp[i++] = a[begin1++];
		}
	}

	//将剩余数据拷贝至tmp
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		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);
}

2.归并排序的非递归实现循环

        这里我们使用循环模拟递归分割数组的过程,首先我们要将数据分割为一个,然后两个数据合并完毕之后,控制begin跳到第三个数的位置,然后对第三四个数据进行归并,重复此步骤我们就完成了最开始的一趟合并。接下来,我们的数据就变成两个两个一组了,我们需要控制begin一次跳4个数据来进行排序。重复这个过程直至最后剩下两个区间,合并完成就是有序的数组。

        代码实现,这里我们定义一个gap来控制begin移动的大小,每次合并完更换合并区间时只需要更改gap*=2即可。非归并排序会出现各种各样的越界问题,因此我们在这里对越界进行详细分析当 end1 越界时,我们要将end1拉回区间,让end1 = n-1(n为数组大小);同时我们要将begin2和end2设置为不存在区间防止其越界。  当end1没有越界begin2越界时我们需要将begin2和end2设置为不存在区间。当只有end2越界时只需要将end2拉回区间即可,即让end2 = n-1 。

        代码实现如下:

//非递归
void MergeSortNonr(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//开tmp 数组来临时存储归并好的数据
	if (tmp == NULL)
	{
		perror("malloc false");
		exit(-1);
	}
	int gap = 1;

	while (gap < n)
	{
		int j = 0;
		for (int 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)//越界处理 -- 1
			{
				end1 = n - 1;
				//不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)//end2复位//防止越界
			{
				//不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else 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, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}
  • 47
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小白_moon

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

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

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

打赏作者

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

抵扣说明:

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

余额充值