保姆级实现常见的排序算法(递归与非递归版,超详细!!!)

目录

常见的排序算法

一、插入排序

1.直接插入排序

2.希尔排序

二、选择排序

1.选择排序

2.堆排序 

三、交换排序 

1.冒泡排序

 2.快速排序

 四、归并排序

五、计数排序


常见的排序算法

        常见的排序算法有插入排序(直接插入排序,希尔排序),选择排序(选择排序,堆排序),交换排序(冒泡排序,快速排序),归并排序,以及计数排序。本文将会详细介绍上述排序(按照升序的形式)以及它们的实现形式(递归与非递归)。

一、插入排序

1.直接插入排序

思想

        直接插入排序是从需要排序的数组的一个元素开始,每次开始比较的数记作(x),将它与之前(直到第一个元素)依次比较,如果这个数比本次x大,那么便向后移动一个距离。我们从第一个元素开始把它作为x的,便保证了数组的有序,当我们找到比这个数小的数的时候便可以停下将停下的这个数后面的位置放入我们x。这样依次遍历,直到到数组的最后一个元素,便可以完成数组的排序。

时间复杂度与空间复杂度

        我们可以看出,在最好的情况下,即原数组为升序的情况下,插入排序只需要遍历一次数组,最好的时间复杂度为O(N)。而最坏的情况里便是数组为逆序,则每个数都要与前面的数进行交换,这样最坏的时间复杂度就是O(N^2)。所以插入排序的时间复杂度在这两者之间。

        我们并没有开辟额外的空间,所以空间复杂的为O(1)。

代码的实现

        为了保证代码的实现,我们在这里可以从第一个数开始,把它的后一个数作为x,依次与前面的数进行比较。

void InsertSort(int* a, int n)
{
	assert(a);
	for (int i = 0; i < n - 1;i++)
	{
		
		int end = i;
		int x = a[end + 1];
		while (end >= 0)
		{
			if (a[end]>x)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = x;
	}
}

2.希尔排序

思想

        希尔排序算法是对直接插入排序算法的优化。当时他想如果让这个数组接近有序,便可以提升这个算法的效率,于是他想出了一个方法。那便是定义一段步长(我们记作gap)让数组中每一组间隔gap的数字有序,而这个gap的定义可以由大到小依次变化,直到为1便成为了直接插入排序。而此时数组接近有序,直接插入排序的算法便可以得到优化。

代码的实现

        我们在取gap的时候,可以如给出的代码这样取。这样取可以每次减少gap的值,而+1保证了gap的最后值为1。也可以每次都gap=gap/2。

        而我们的每次从0开始,让每个数的后一个数(x)与前面每一个相差步长为gap的数进行比较。这样便可一次遍历使数组更接近有序。

void ShellSort(int* a, int n)
{
	int gap = n;
	while(gap>1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i += 1)
		{
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = x;
		}
	}
}

二、选择排序

1.选择排序

思想

        选择排序是每次选出最大值或者最小值。也可以同时选出,这里我们给出的算法是同时选出最大值与最小值,把它们分别放到首尾位置,再从除去最大值最小值的数组中再次按照上述方法进行。直到完成排序。

在这里插入图片描述

代码的实现: 

void Swap(int* num1, int* num2)
{
	int tem = *num2;
	*num2 = *num1;
	*num1 = tem;
}
void SelectSort(int* a, int n)
{
	int mini;
	int maxi;
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		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 (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

2.堆排序 

思想

        堆排序是利用数据结构的堆来进行排序,它实际上控制的物理结构是一个一维数组,而我们要把堆给想象成完全二叉树进行控制。同时堆排序的时间复杂度为O(N)是一种效率极高的排序方法。同时我们在需要排序的数组进行操作即可,不需要向内存申请额外的空间,所以空间复杂度为O(1)。

代码的实现

         我们只需要利用一个向下调整算法即可完成堆排序。

        如果我们需要排升序,把原数组调整为大堆,再利用堆结构的pop思想即可完成操作。

        堆结构的pop思想是把数组中的第一个元素与最后一个元素进行交换,删除掉最后一个元素,在利用向下调整把删除掉元素后的数组进行重新排序。

        只有有孩子的根节点才可以进行向下调整。我们找到最后一个有孩子的父亲节点,把它和之前的节点依次进行向下调整。在完全二叉树中,度为0的节点比度为2的节点要多一个,我们利用这一个性质来计算最后一个父亲节点的位置,因为只有节点个数为偶数时才会出现度为1的节点,所以我们-1后不会影响结果。

void AdjustDown(int* a, int father,int n)
{
	int child = father * 2 + 1;
	while (child < n)
	{
		if (a[child + 1] > a[child] && child + 1 < n)
		{
			child++;
		}
		if (a[child] > a[father])
		{
			Swap(&a[child], &a[father]);
			father = child;
			child = father * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


void HeapSort(int* a, int n)
{
	for (int i = n-1-1; i >=0; i--)//这里的i为最后一个父亲节点的位置
	{
		AdjustDown(a, i, n);
	}
	for (int i = 0; i < n; i++)
	{
		Swap(&a[0], &a[n - 1 - i]);
		AdjustDown(a, 0, n -1- i);
	}
}

三、交换排序 

1.冒泡排序

 思想

        步骤一:遍历,把最大的数交换到数组的最后。

        步骤二:将数组的大小缩小一。

        步骤三:重复步骤一、二。

代码的实现

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

 2.快速排序

思想

         快速排序是由霍尔提出的,霍尔提出的方法是最初的快速排序方法,后来又衍生出了挖坑法以及前后指针法等。本篇博客将会为读者依次讲解霍尔法,挖坑法,前后指针法。

        我们先给一个无序的一维数组。为升序的思想。

最初的快排方法(霍尔法)

        我们先进行快速排序中单趟排序的讲解。快速排序的思想是先找到一个关键字的值,可以为下标也可以为具体的数,我们通常会选择数组的最左边或最右边作为的关键字,在选择关键字后,假设我们选择左边为关键字。我们需要首先从最右边开始向左寻找,当我们找到一个比关键字小的位置时停下,然后再从关键字的位置开始向右边开始寻找找到一个比关键字大的位置停下,然后交换它们两个。接下来再按照刚才从右边先开始的方法进行寻找,直到左右相遇相遇的位置一定是比关键字小,所以在此交换关键字和最后停留的位置,完成单趟排序

挖坑法
        步骤如下:

我们把坑定在最左边,把这个值进行保留。

        1.从数组最右边开始出发,找到一个比坑小的数,把它放到坑里,让这个数的位置成为新的坑。

        2.从数组最左边开始出发,找到一个比坑大的数,把它放到坑里,让这个数的位置成为新的坑。

        重复步骤1,2直到left,right相遇,把最后一个坑填入一开始保留的值。完成单趟排序,再次按照最初的快排方法的分治思想,从左右两个部分进行挖坑,直到把数组分割成每个数为一个单元。

前后指针法
        步骤如下,我们假设从左边开始定义关键值。

        我们需要一个cur=left+1,一个prev=left,让cur向右找比关键值小的值,找到后与++prev交换。直到cur走出数组。最后返回prev的值。这里需要注意两个地方:一是在我们交换完后cur需要++,否则会进入死循环。再一个是我们要确保cur<=right,防止数组越界。这样也可以完成单趟排序的操作。
 

         完成单趟排序后,我们会发现关键字的左边都比关键字要小,而关键字的右边都比关键字要大,所以关键字来到了一个合适的位置。接下来通过分治的思想,把相遇点的左右两个部分分开,再按照这个方法,最终缩小到只有一个数的时候停下,那么快速排序就完成了。

快排中的一些问题:

1.为什么需要从右边开始寻找比关键字小的数:

        如果从左边先开始,那么停下来的数会比关键字要大,这时无法完成我们单趟排序的需求。

2.快速排序的时间复杂度。
        最坏情况:

        当我们的关键字每次都是数组中最小数时,我们需要分成N层才可以把每个数分成一个单元,而每一层需要遍历这一层数组的个数-1,时间复杂度也就到达了O(N^2)。

        理想状况:

        每次选取的关键字位于数组中间,每层分时对半分,可以想象成二叉树,共会分成log2N层,时间复杂度也就到了O(N*log2N)。

        所以快速排序的时间复杂度我们一般取O(N*logN)。

3.如何避免最坏情况。
        我们可以利用一个getmid函数,来获取left,right,mid的中间值作为我们的关键字,这样就会有效避免最坏情况的发生。

4.小区间优化。

        快速排序的算法比较像二叉树,越往下需要的次数越多,因此我们可以进行小区间优化,当区间很小的时候,我们直接运用插入排序即可。这样就会对快排算法进行优化

代码的实现

这里我们先给出递归实现的方法。

int Partion1(int* a, int left, int 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[keyi], &a[left]);
	return left;
}
int Partion2(int* a, int left, int right)//挖坑法
{
	int pivot = left;
	int key = a[left];
	while (left < right)
	{
		while (left < right && a[right] >= key)
			right--;
		a[pivot] = a[right];
		pivot = right;
		while (left < right && a[left] <= key)
			left++;
		a[pivot] = a[left];
		pivot = left;
	}
	a[pivot] = key;
	return pivot;
}
 
int GetMidNum(int* a, int left, int right)//获取三个数的中间值
//避免最坏情况
{
	int mid = (left + right) / 2;
	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 right;
		else
			return mid;
	}
}
int Partion3(int* a, int left, int right)//前后指针法
{
	int cur = left+1;
	int keyi = left;
	int prev = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			Swap(&a[++prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
 
	return prev;
}
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
		return;
	int TheMid = GetMidNum(a, left, right);
	Swap(&a[TheMid], &a[left]);
	
	int keyi = Partion1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi+1, right);
}

非递归实现的方法:

        我们可以利用栈来完成快排的非递归算法。首先我们把数组的左右区间下标(这里我们先放入的左下标,再放入的右下标,那么出栈时便是先出右下表,再出左下标)放入到栈中,每次取出一组下标,让每一组中选出一个keyi值来,如果keyi的左右区间是合法的,那么再把左右区间放入到栈中,如果不合法,则不把这个区间放入栈中。直到栈空,非递归实现快排的算法即可实现。

        这里我们运用到了栈,可以参考博主之前发的实现栈的文章。

void QuickSortNonR(int* a, int left, int right)
{	
	ST* theStack = (ST*)malloc(sizeof(ST));
	StackInit(theStack);
	StackPush(theStack, left);
	StackPush(theStack, right);
	while (!StackEmpty(theStack))
	{
		int end = stackTop(theStack);
		StackPop(theStack);
		int begin = stackTop(theStack);
		StackPop(theStack);
		int keyi = Partion3(a, begin, end);
		if (keyi + 1 < end)
		{
			StackPush(theStack, keyi + 1);
			StackPush(theStack, right);
		}
		if (begin < keyi - 1)
		{
			StackPush(theStack, left);
			StackPush(theStack, keyi - 1);
		}
	}
	StackDestory(theStack);
}

 四、归并排序

思想

        我们先把数组进行拆分成多个小区间,让小区间有序。再把多个小区间通过归并合并成一个大区间,让小区间上的上一层大区间有序。直到大区间为整个数组,即可完成归并排序。

 代码的实现

递归实现:

这里我们运用一个子函数来实现递归。

void _MergeSort(int* a, int left, int right,int*tem)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tem);
	_MergeSort(a, mid+1, right, tem);
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tem[i++] = a[begin1++];
		}
		else
		{
			tem[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tem[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tem[i++] = a[begin2++];
	}
	for (int j = left; j <= right; j++)
	{
		a[j] = tem[j];
	}
}
void MergeSort(int* a, int left, int right)
{
	int* tem = (int*)malloc(sizeof(int) * (right - left+1));
	if (tem == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a, left, right, tem);
}

非递归实现 :

        这里的思想与递归是相似的。我们运用一个循环来实现非递归。我们首先把步长定为1,先让区间大小为1的两个区间归并到tem数组,再把tem数组中有的值考回到原数组。再扩大gap的倍数来进一步增大归并的区间,直到区间覆盖原数组。

边界情况

        在这里,我们的left1永远不会越界,所以也就会产生三种越界情况,即right1越界,left2越界,right2越界,当right1越界的时候,我们不需要再往下进行了,因为我们是每执行一次便把tem数组考回原数组。所以当right1越界我们就直接跳出。并且我们是从区间为1开始的。当right2越界并且left2未越界时,我们便需要进行归并,这是我们修正right2即可,把right2修正为right即可。

        另一种思路便每次都把tem数组中的所有值都考回去,这样的话需要对三种越界情况都进行修正,这里没有给出相关代码,感兴趣的读者可以自己写一下。

void MergeSortNonR(int* a, int left, int right)
{
	int n = right - left + 1;
	int* tem = (int*)malloc(sizeof(int) * n);

	if (tem == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	int gap = 1;

	while (gap < n)
	{
		for (int i = 0; i <= right; i += 2 * gap)
		{
			int left1 = i;
			int right1 = i + gap - 1;
			int left2 = i + gap;
			int right2 = i + 2 * gap - 1;
			if (right1 >= right)
			{
				continue;
			}
			if (right2 >= right && left2 <= right)
			{
				right2 = right;
			}
			int j = i;
			while (left1 <= right1 && left2 <= right2)
			{
				if (a[left1] <= a[left2])
				{
					tem[j++] = a[left1++];
				}
				else
				{
					tem[j++] = a[left2++];
				}
			}
			while (left1 <= right1)
			{
				tem[j++] = a[left1++];
			}
			while (left2 <= right2)
			{
				tem[j++] = a[left2++];
			}
			for (j = 0; j <= right2; j++)
			{
				a[j] = tem[j];
			}

		}
		gap *= 2;
	}
	free(tem);
	tem = NULL;
}

五、计数排序

思想

        计数排序的思想类似于哈希表。适合运用在极值范围比较小的区间,我们利用相对映射的关系,把原数组中拥有的数的个数统计到一个count数组中,再根据count数组与原数组的映射关系,把正确的值再放回到原数组。

代码的实现

void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	memset(count, 0, sizeof(int) * range);
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

 

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

__gold

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

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

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

打赏作者

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

抵扣说明:

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

余额充值