所有常见排序详解

1.排序的意义

1.1排序的概念

在这里插入图片描述

1.2排序的应用

在这里插入图片描述

在这里插入图片描述

1.3常见的排序算法

在这里插入图片描述

2.插入排序

2.1 基本思想

在这里插入图片描述

2.2直接插入排序

在这里插入图片描述

// 插入排序
// 时间复杂度是多少?O(N^2)
// 什么情况下最坏?逆序  1+2+3+...+n-1
// 什么情况下最好?顺序有序  O(N)
void InsertSort(int* a, int n)
{
	// [0, end]有序  end+1位置的值插入[0, end],让[0, end+1]有序
	for (int i = 0; i < n-1; ++i)   /// 注意这里的 i< n-1 
	{								/// 从 第二个数开始与前面的比,一个元素时就是有序
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		// tmp 插入中间,或tmp比所有的值大插入最前面,或tmp比所有的值小插入最后面
		// 无论哪一个画图都可以看到tmp要插到end后面
		a[end + 1] = tmp;
	}
}

2.3希尔排序(缩小增量排序)

在这里插入图片描述

// 直接插入排序的基础上的优化
// 1、先进行预排序,让数组接近有序
// 2、直接插入排序
// 时间复杂度:O(logN*N) 或者 O(log3N*N)
// 平均的时间复杂度是O(N^1.3)
[9 1 2 5 7 4 8 6 3 5]
第一层循环用于分gap,第一趟5,第二趟2,第三趟 1
第二个循环用于遍历数组n-gap个元素  比如 gap=2 [4 1 2 5 9 8 6 5 7]会遍历到6结束
第三个循环当为i时,第i+gap与前面的间隔gap的所有元素个数比较  与i,i-gap...比
比如 gap=2 [4 1 8 0 9 6 5 2 7] a[i]=9 < a[i+2]=5 
原来【4<8<9】但是95交换后 【4<8<5<9】破坏了原来的顺序,所以 5还需要继续往前比较,直到遇到一个不满住交换条件直接跳出,因为更前面一定满足
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//gap = gap / 2;  // logN
		gap = gap / 3 + 1; // log3N 以3为底数的对数
		// gap > 1时都是预排序  接近有序
		// gap == 1时就是直接插入排序 有序

		// gap很大时,下面预排序时间复杂度O(N)
		// gap很小时,数组已经很接近有序了,这时差不多也是(N)

		// 把间隔为gap的多组数据同时排

		for (int i = 0; i < n - gap; ++i) 
		{
			int end = i;
			int tmp = a[end + gap];
			a[i] < a[i+2] 95交换 [4 1 8 0 5 6 9 2 7] 但是 a[i]=5,a[i-2]=8 
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}	
}

3.选择排序

3.1基本思想

在这里插入图片描述

3.2直接选择排序

在这里插入图片描述

下面的直接选择排序优化了一下,一次循环找最小和最大的两个元素,而上面的图解是一次循环只找最大的元素丢到后面去。
下面的找最小和最大要注意:begin跟maxi重叠,需要修正一下maxi的位置

// 直接选择排序,时间复杂度O(N*N)
// 很差,因为最好情况也是O(N*N)
// N
// N-2
// N-4
// ...
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int mini = begin, maxi = begin;
		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]);
		// 如果begin跟maxi重叠,需要修正一下maxi的位置
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		++begin;
		--end;
	}
}

在这里插入图片描述

3.3堆排序

在这里插入图片描述

在这里插入图片描述

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void AdjustDwon(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1; // 默认是左孩子
	while (child < n)  
	{
		// 1. 选出左右孩子中大的哪一个
		// 注意判断条件 child + 1 < n
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child += 1;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
// 小堆要求:树中所有的父亲都大于等于孩子
// 大堆要求:树中所有的父亲都小于等于孩子
/// 建小堆--向下调整算法--前提:左右子树都是小堆

void HeapSort(int* a,int n)
{
	// 当左右子树不是小堆,使用下面方法
	// 倒着从最后一颗子树开始调,叶子不需要调,从倒数最后一个非叶子的子树开始调

	/// (n-1)最后一个孩子,(n-1-1)/2 倒数第一个根
	/// 循环 从后依次找到每个根 ,用向下调整算法
	///  O(N) 建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDwon(a, n, i);
	}
	///如过建小堆,最小数在堆顶,已经被选出来了,那么在剩余的数中再去选数,但是剩余数结构
	///都乱了,需要重建堆才能选出下一个数,建堆的时间复杂度是O(N),那么这样不是可以
	/// 但是堆排序就没有优势了
	
	// 升序建大堆  降序建小堆
	// 第一个和最后一个交换 把他不看做堆里面
	/// 排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDwon(a, end, 0);
		--end;
	}
}

4.交换排序

4.1基本思想

在这里插入图片描述

4.2冒泡排序

在这里插入图片描述

/// 冒泡排序  对局部有序插入排序比冒泡排序的适应性更强
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n - 1; j++) // n-1趟
	{
		int exchange = 0;
		for (int i = 1; i < n - j; i++) // 每趟要比较的个数
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}

}

4.3快速排序

在这里插入图片描述
在这里插入图片描述

4.3.1递归法

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];

	// ... 单趟排序
	while (begin < end)
	{
		// 找小
		while (begin < end && a[end] >= a[keyi])
		{
			--end;
		}

		// 找大
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}

		Swap(&a[begin], &a[end]);
	}

	Swap(&a[begin], &a[keyi]);

	// [left, right]
	// [left, pivot-1] pivot [pivot+1, right]
	// 左子区间和右子区间有序,我们就有序了,如果让他们有序呢? 分治递归
	QuickSort(a, left, pivot - 1);
	QuickSort(a, pivot + 1, right);
}

4.3.2递归+三数取中+小区间优化

这里有三种单躺排序

  1. 挖坑法
  2. 前后指针法
  3. 左右指针法
// 三数取中
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;
	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;
		}
	}
}

// 挖坑法
int PartSort1(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]); // // 为了让key还是选第一个  注意 &a[left]中 是left

	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];

	// O(N)
	while (begin < end)
	{
		// 右边找小,放到左边(注意判断条件  begin < end)
		while (begin < end && a[end] >= key) // 注意 a[end] >= key 没有等于可能会死循环
			--end;							 // 注意 begin < end 没有  可能会越界

		// 小的放到左边的坑里,自己形成新的坑位
		a[pivot] = a[end];
		pivot = end;

		// 左边找大
		while (begin < end && a[begin] <= key)
			++begin;

		// 大的放到左边的坑里,自己形成新的坑位
		a[pivot] = a[begin];
		pivot = begin;
	}

	pivot = begin;
	a[pivot] = key;

	return pivot;
}

// 前后指针
int PartSort2(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);

	int begin = left, end = right;
	int keyi = begin;

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

		// 找大
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}

		Swap(&a[begin], &a[end]);
	}

	Swap(&a[begin], &a[keyi]);

	return begin;
}
/// 左右指针
int PartSort3(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);

	int keyi = left;
	int prev = left, cur = left + 1;
	while (cur <= right) // 闭区间小于等于
	{
		//if (a[cur] < a[keyi])
		//{
		//	prev++;
		//	Swap(&a[prev], &a[cur]);
		//}
		if (a[cur] < a[keyi] 
			&& ++prev != cur)  /// 与前面注释的部分相比,这种方法不用自己交换自己
		{					  ///  a[cur] < a[keyi] 注意是小于,与前面的a[end]<=key不同
			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 keyIndex = PartSort3(a, left, right);

	// [left, right]
	// [left, keyIndex-1] keyIndex [keyIndex+1, right]
	// 左子区间和右子区间有序,我们就有序了,如果让他们有序呢? 分治递归

	// QuickSort(a, left, keyIndex - 1);
	// QuickSort(a, keyIndex + 1, right);

	// 小区间
	// 当数据量很大的时候,最后几层递归的数量很大
	// 使用小区间优化
	if (keyIndex - 1 - left > 10)
	{
		QuickSort(a, left, keyIndex - 1);
	}
	else
	{
		InsertSort(a + left, keyIndex - 1 - left + 1);
	}

	if (right - (keyIndex + 1) > 10)
	{
		QuickSort(a, keyIndex + 1, right);
	}
	else
	{
		InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);
	}
}

4.3.3非递归+三数取中

递归的缺陷:栈帧深度太深,栈空间不够用,可能会溢出

  • 递归改非递归:1、直接改循环(简单) 2、借助数据结构栈模拟递归过程(复杂一点)
  • 要用到栈的接口函数:栈的函数实现博客
void QuickSortNonR(int* a, int n)
{
	ST st;
	StackInit(&st);
	StackPush(&st, n - 1);
	StackPush(&st, 0);

	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);

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

		int keyIndex = PartSort1(a, left, right);
		// [left, keyIndex-1] keyIndex [keyIndex+1, right]

		if (keyIndex + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyIndex + 1);
		}

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

	StackDestory(&st);
}

5.归并排序

5.1基本思想

在这里插入图片描述
在这里插入图片描述

5.2代码

归并的递归形式几乎是典型的二分

在这里插入图片描述

// 时间复杂度:O(N*logN)   
// 空间复杂度:O(N)
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;

    // [begin, mid] [mid+1, end] 分治递归,让子区间有序
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

    //归并 [begin, mid] [mid+1, end]
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = 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++];
	}

	// 把归并数据拷贝回原数组
	memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

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

	free(tmp);
}

5.3归并非递归

5.3.1为什么不能使用栈

归并的非递归不能用栈和队列来实现,栈和队列实现非递归适合前序不适合后续。
想一想前面的非递归快速排序,它是使用栈来实现的,思路是选出一个key分出左区间和右区间,左区间右区间都处理后就有序了,不需要回来后再做其他事

情。而归并排序递归回来还要做一些事情。非递归快速排序相当于前序,归并的非递归相当与后续

在这里插入图片描述

5.3.2非递归实现思路

在这里插入图片描述

5.3.3注意下面两个代码实现非递归memcpy在代码的位置

代码一(注意归并完后一起拷贝回元素组)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		printf("gap=%d->", gap);
		for (int i = 0; i < n; i += 2 * gap)
		{
			// [i,i+gap-1][i+gap, i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 越界-修正边界
			if (end1 >= n)
			{
				end1 = n - 1;
				// [begin2, end2]修正为不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				// [begin2, end2]修正为不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);

			int j = begin1;
			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++];
			}
		}

		printf("\n");
		memcpy(a, tmp, sizeof(int) * n);

		gap *= 2;
	}

	free(tmp);
}
代码二(归并一部分拷贝一部分)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	// 休息11:48继续
	int gap = 1;
	while (gap < n)
	{
		//printf("gap=%d->", gap);
		for (int i = 0; i < n; i += 2 * gap)
		{
			// [i,i+gap-1][i+gap, i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// end1越界或者begin2越界,则可以不归并了
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			//printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);

			int m = end2 - begin1 + 1;
			int j = begin1;
			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)* m);
		}

		gap *= 2;
	}

	free(tmp);
}

5.3.4归并排序的特性总结

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

6. 测试排序的性能对比

每个函数的接口实现前面都有相关代码

// 排序实现的接口
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
比特就业课void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n)
// 快速排序
void QuickSort(int* a, int left, int right);
// 归并排序
void MergeSort(int* a, int n)

// 测试排序的性能对比
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int)*N);
int* a2 = (int*)malloc(sizeof(int)*N);
int* a3 = (int*)malloc(sizeof(int)*N);
int* a4 = (int*)malloc(sizeof(int)*N);
int* a5 = (int*)malloc(sizeof(int)*N);
int* a6 = (int*)malloc(sizeof(int)*N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
}

int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();

int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();

int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();

int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();

int begin5 = clock();
QuickSort(a5, 0, N-1);
int end5 = clock();

int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();

printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}

排序的稳定性

在这里插入图片描述

非比较排序

注意前面的排序都是比较排序,元素之间会通过比较大小进行排序

下面的排序时非比较排序--其排序只试用与整数
  - 桶排序
  - 计数排序
  - 基数排序

计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

局限性:

  1. 如果是浮点数或字符串就不能用了
  2. 如果数据范围很大,空间复杂度就会很高
  3. 时间复杂度:O(MAX(N,范围))
  4. 空间复杂度:O(范围)

在这里插入图片描述

// 时间复杂度:O(max(range, N))
// 空间复杂度:O(range)
void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 1; i < n; ++i)
	{
		if (a[i] < min)
			min = a[i];

		if (a[i] > max)
			max = 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)
	{
		// 出现几次就会回写几个i+min
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

基数排序

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值