八大排序算法:插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序

1、插入排序

步骤:

1.从第一个元素开始,该元素可以认为已经被排序
2.取下一个元素tmp,从已排序的元素序列从后往前扫描
3.如果该元素大于tmp,则将该元素移到下一位
4.重复步骤3,直到找到已排序元素中小于等于tmp的元素
5.tmp插入到该元素的后面,如果已排序所有元素都大于tmp,则将tmp插入到下标为0的位置
6.重复步骤2~5
动图演示:

 基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。

代码如下:


// 最坏的情况是O(N*N)
// 在有序的情况下是O(N)

void InsertSort(int* a, int n)
{
	// 在[0,end] 插入end+1,保持有序
	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];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

1、待排序为逆序(或者接近逆序的情况下)时间复杂度为O(N*N)

2、待排序为顺序(或者接近顺序的情况下)时间复杂度为O(N)

3、时间复杂度为O(1)

4、稳定性:非常稳定

2、希尔排序(缩小增量排序)

步骤:

1、先选定一个小于N的整数为gap作为第一增量,之后间隔为gap的为一组

2、当gap > 1时为预排序

3、当gap = 1时就全部排好了

代码如下:


void ShellSort(int* a, int n)
{
	int gap = n;
	// gap > 1,表示预排序
	// gap = 1,表示直接插入
	while (gap > 1)
	{
		gap = gap / 2;
		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;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

时间复杂度为O(N^1.3)

空间复杂度为O(1)

效率非常快

基本思想:先选定一个整数,把待排序文件中所有记录分成N个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

3、选择排序

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

动图演示:

 代码如下:


void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		// 选出最小的放在begin的位置
		// 选出最大的放在end的位置
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; ++i)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		// 修正一下
		if (maxi == begin)
			maxi = mini;
		// 交换 
		Swap(&a[begin], &a[mini]);
		Swap(&a[end], &a[maxi]);
		++begin;
		--end;

	}

}

4、冒泡排序

基本思路:两两比较,大的根小的交换,大的放在后边,小的放在最前面,交换一趟下来最大的排在最右边。

// 交换函数,把大的放在后面,小的放在前面
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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

5、堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆

大堆:每个节点的数值都大于或者等于它左右节点的数值,所以堆顶的数值最大。

小堆:每个节点的数值都小于或者等于它左右节点的数值,所以堆顶的数值最小。

基本思路:

1、对原数组进行大堆构造

2、交换头尾指针的值,尾指针依次递减,直到找到最大值

3、重复构造大堆

4、直到排到最后一个排序完成


// 交换函数,把大的放在后面,小的放在前面
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
	int minChild = parent * 2 + 1;
	while (minChild < n)
	{
		// 找出小的那个孩子
		if (minChild + 1 < n && a[minChild + 1] > a[minChild])
		{
			minChild++;
		}

		if (a[minChild] > a[parent])
		{
			Swap(&a[minChild], &a[parent]);
			parent = minChild;
			minChild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// O(N*logN)
void HeapSort(int* a, int n)
{
	// 大思路:选择排序,依次选数,从后往前排
	// 升序 -- 大堆
	// 降序 -- 小堆
	// 建堆 -- 向下调整建堆 - O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	// 选数 N*logN
	int i = 1;
	while (i < n)
	{
		Swap(&a[0], &a[n - i]);
		AdjustDown(a, n - i, 0);
		++i;
	}
}

6、快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。

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

6.1 hoare版本

基本思路:

1、选出一个key(一般是最左边或者最右边)。

2、定义一个left和一个right,left从左向右走,right从右向左走。

3、left找比key大的数,right找比key小的数,right找到比key小的停下来,让left找,找到比key大的也停下来,然后交换两边的数,重复上述步骤,直到左边比key小,右边比key大。

4、一直进行单趟排序,直到整个数组有序。

单趟动图演示:

 代码如下:


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

// hoare法
int PartSort1(int* a, int left, int right)
{
	// 三数取中
	int mid = GetMidIndex(a,left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			// R找小
			--right;
		}
		while (left < right && a[left] <= a[keyi])
		{
			// L找大
			++left;
		}
		if(left < right)
	     	Swap(&a[left], &a[right]);
	}
	int meeti = left;
	// 交换相遇后的位置跟key
	Swap(&a[meeti], &a[keyi]);

	return meeti;

}

void QuickSort(int* a, int begin, int end)
{
     int keyi = PartSort1(a, begin, end);
		// 分成[begin,keyi-1]  [keyi+1,end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
}

6.2挖坑法

基本思路:

选择最左边或者最右边存放临时变量key,形成一个坑位,然后右边找小,填到左边的坑位去,后面的思想跟hoare类似,这里就不多说了。

单趟动图演示:


// 挖坑法
int PartSort2(int* a, int left, int right)
{
	// 三数取中
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		// 右边找小,填到左边坑位
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[hole] = a[right];
		hole = right;

		// 左边找大,填到右边坑位
		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[hole] = a[left];
		hole = left;

	}
	// 在相遇的地方,把key填上
	a[hole] = key;
	return hole;
}

// 时间复杂度是O(N*logN)
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end - begin <= 8)
	{
		InsertSort(a+begin, end - begin +1);
	}
	else
	{
		int keyi = PartSort2(a, begin, end);
		// 分成[begin,keyi-1]  [keyi+1,end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	
}

 上面的算法运用了快速排序的优化,用了三数取中和小区间优化。

6.3前后指针法

思路:

1、选择一个key(一般是最左边或者最右边)。

2、prev指向开始key的位置,cur指向prev前面,cur找比key小,prev紧跟cur,prev指向比key大的值得前面,然后++cur,然后就可以交换cur和prev,一直重复此步骤,直到cur走到最后一个值,或者左右的序列不存在就停下来。

动图演示:

 代码如下:


// 前后指针法
int PartSort3(int* a, int left, int right)
{
	// 三数取中
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[cur],&a[prev]);
		++cur;
	}
	Swap(&a[keyi],&a[prev]);
	return prev;
}

// 时间复杂度是O(N*logN)
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end - begin <= 8)
	{
		InsertSort(a+begin, end - begin +1);
	}
	else
	{
		int keyi = PartSort2(a, begin, end);
		// 分成[begin,keyi-1]  [keyi+1,end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	
}

6.4快排的非递归

为什么要学习非递归呢?

当数据量非常大而且接近有序时,这时在使用快排,就会使得递归调用的深度非常深,而且它们都是有序(或者接近有序),使得快排非常尴尬,这时的快排速度比冒泡排序还要慢。使用学习非递归就非常重要了。

那么非递归要怎么实现呢,这里我们就使用栈模拟二叉树的递归,看似不是递归胜似递归。


void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st); // 初始化栈
	StackPush(&st, begin); // 先入左再入右,栈就会先出左再出右
	StackPush(&st, end);  

	while (!StackEmpty(&st)) // 栈不为空时进来
	{
		int right = StackTop(&st);
		StackPop(&st);

		int left = StackTop(&st);
		StackPop(&st);

		/*if (left >= right)
		{
			continue;
		}*/

		int keyi = PartSort3(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);


}

7、归并排序(递归与非递归)

什么是归并排序呢?

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

下面我们来看一下图,更好理解思想

递归版的

代码如下:


_Merge(int* a, int begin1, int end1, int begin2, int end2,int* tmp)
{
	int i = begin1;
	int j = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		// 把数组对应的值进行比较
		if (a[begin1] < a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];

	}

	while (begin1 <= end1)
		tmp[i++] = a[begin1++];

	while (begin2 <= end2)
		tmp[i++] = a[begin2++];
	// 归并后拷贝回去
	for (; j <= end2; ++j)
	{
		a[j] = tmp[j];
	}
}
_MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	// 区间分为[left,mid] [mid+1,right]
	int mid = left + (right - left) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid+1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;

	_Merge(a, left, mid, mid + 1, right, tmp);

}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;

}

7.1归并排序(非递归版)

我们把数分为gap组,gap依次增大,直到后面把数组全部排有序了就可以了。

这时候我们就要划分区间了,下面我们来看图

 但是我们可能会遇到一些问题(左边),解决方案(右边)如图

代码如下:


void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	int gap = 1; // 先初始化gap为1

	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap) // 每次让i跳过2*gap
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 归并到最后一个小组的第一、二个区间不存在时就结束循环
			if (begin2 >= n)
				break;

			// 当最后一个小组的第二个区间不够gap个时,修正一下
			if (end2 >= n)
				end2 = n - 1;

			_Merge(a, begin1, end1, begin2, end2, tmp);
		}
		gap *= 2;// 每次让gap变为原来的二倍
	}

	free(tmp);
	tmp = NULL;
}

8、计数排序(非比较)

用于数比较集中正整数的时候,当数中有正负数时不推荐使用,当数组中出现浮点数时也不推荐使用。

计数排序的特性:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。

2. 时间复杂度:O(MAX(N,范围))

3. 空间复杂度:O(范围)

4. 稳定性:稳定

代码如下:


void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; ++i)
	{
		if (a[i] > max)
			max = a[i];

		if (a[i] < min)
			min = a[i];
	}

	// 闭区间所以要加1
	int range = max - min + 1;
	int* count = malloc(sizeof(int) * range);
	
	// 把count里的数组全部置成0
	memset(count, 0, sizeof(int) * range);

	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);
	count = NULL;
}

猜你喜欢

转载自blog.csdn.net/weixin_64214213/article/details/126388423