深入学习C语言中常见的八大排序

目录
  • 冒泡排序
    • 1.算法描述
    • 2.动图展示
    • 3.图解展示
    • 4.代码实现
    • 5.冒泡排序的优化
    • 6.复杂度分析
  • 插入排序
    • 1.算法描述
    • 2.动图展示
    • 3.图解展示
    • 4.代码实现
    • 5.复杂度分析
  • 希尔排序
    • 1.算法描述
    • 2.动图展示
    • 3.图解展示
    • 4.代码实现
    • 5.复杂度分析
  • 堆排序
    • 1.算法描述
    • 2.动图展示
    • 3.图解展示
    • 4.堆的一些基本性质
    • 5.堆的构造
    • 6.代码实现
    • 7.复杂度分析
  • 选择排序
    • 1.算法描述
    • 2.动图展示
    • 3.图解展示
    • 4.代码实现
    • 5.复杂度
  • 快速排序
    • 1.算法简介
    • 2.动图展示
    • 3.图解展示
    • 4.代码实现
    • 时间复杂度
  • 归并排序:
    • 1.算法分析
    • 2.动图展示
    • 3.图解展示
    • 非递归:
    • 实现思路
  • 排序总结

排序是非常重要的内容,一般来说,我们经常用到的也就是十大排序,如图所示

按照比较类和非比较类又可以分为:

冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来

1.算法描述

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

2.动图展示

3.图解展示

4.代码实现

void BubbleSort(int* a, int n) {
	for (int i = 0; i < n - 1; i++) {

		for (int j = 0; j < n - 1 - i; j++) {
			if (a[j] > a[j + 1]) {

				swap(a[j], a[j + 1]);
			}
		}

	}
}

5.冒泡排序的优化

如果我们的冒泡排序比较了一圈之后发现没有发生交换,说明此时已经有序了。我们就可以退出循环。

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;
		}
	}
}

6.复杂度分析

时间复杂度:O(N^2)

若数组为倒序,即所有的轮次都必须执行完(最坏情况),比较次数为 n-1 + n-2 +...+ 1 = n(n-1)/2,交换次数与比较次数相同,所以时间复杂度为O(N^2)

空间复杂度:O(1)

稳定性:插入排序是稳定的排序算法,满足条件nums[j] > nums[j + 1]才发生交换,这个条件可以保证值相等的元素的相对位置不变

插入排序

插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。 按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 。插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

1.算法描述

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)

2.动图展示

3.图解展示

4.代码实现

void Insert(int* a, int n)
{
	for (int i = 0; i < n - 1; i++) {

		int j = 0;
		int tmp = a[i + 1];

		for (j = i; j >= 0 && tmp < a[j]; j--) {
			a[j + 1] = a[j];
		}
		a[j + 1] = tmp;
	}
}

或者也可以这样写,这样写逻辑更清晰:

void Insert(int* a, int n)
{
	for (int i = 0; i < n - 1; i++) {

		int end = i;
		int tmp = a[end + 1];
		while (end >= 0) {
			if (tmp < a[end]) {
				a[end + 1] = a[end];
				--end;
			}
			else {
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

5.复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1),只需要一个额外空间用于交换
  • 稳定性:插入排序是稳定的排序算法,满足条件nums[j] > nums[j + 1]才发生交换,这个条件可以保证值相等的元素的相对位置不变

希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

1.算法描述

选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;

按增量序列个数 k,对序列进行 k 趟排序;

每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度

2.动图展示

3.图解展示

或者:

4.代码实现

void ShellSort(int* a, int n) {
	int gap = n;
	while (gap > 1) {
		gap = gap / 3 + 1;
		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;
	  }

	}
}

5.复杂度分析

1、时间复杂度:O(N^2)
希尔排序最坏的时间复杂度依然为 O ( N 2 )
但其能够有效改善直接插入排序序列无序且长度大时的大长度数列移位。希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能,本文使用的是希尔增量,还有 Hibbard 增量,时间复杂度为 O(N^1.5) 也可以认为是O ( N^log N)空间复杂度:O(1)
未借助其它辅助空间。

稳定性分析:
与直接插入排序不同,希尔排序中的分组插入可能导致顺序移位。

所以,插入排序是稳定的排序算法。

堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

1.算法描述

① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点
② 依次将根节点与待排序序列的最后一个元素交换
③ 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列 先将初始的R[0…n-1]建立成最大堆,此时是无序堆,而堆顶是最大元素。
再将堆顶R[0]和无序区的最后一个记录R[n-1]交换,由此得到新的无序区R[0…n-2]和有序区R[n-1],且满足R[0…n-2].keys ≤ R[n-1].key
由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
直到无序区只有一个元素为止。

2.动图展示

3.图解展示

1.预备知识:

堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序,下面先来看看什么是大根堆和小根堆

2.大根堆和小根堆:

性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。如下图

对应成数组便是:

4.堆的一些基本性质

查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么

1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)

2.左孩子索引:2*i+1

3.右孩子索引:2*i+2

所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:

大根堆:arr(i)>arr(2*i+1) && arr(i)>arr(2*i+2)

小根堆:arr(i)<arr(2*i+1) && arr(i)<arr(2*i+2)

5.堆的构造

将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)

假设存在以下数组

主要思路:第一次保证0~0位置大根堆结构(废话),第二次保证0~1位置大根堆结构,第三次保证0~2位置大根堆结构...直到保证0~n-1位置大根堆结构(每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端)

插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大根堆结构,如下图:

插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构,如下图

插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构,如下图

插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构

此时,我们已经得到一个大根堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大根堆

此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较

下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大根堆,后面就是重复固定最大值,然后构造大根堆

如下图:顶端数7与末尾数3进行交换,固定好7,

剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组

重点建堆的时间复杂度:因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

6.代码实现

void AdjustDown(int*a,int root,int n) {
	int parent = root;
	int child = 2 * parent + 1;//向下调整算法
	while (child < n) {
		if (child+1<n&&a[child] < a[child + 1]) {
			++child;
		}
		if (a[child] > a[parent]) {
			swap(a[child], a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else {
			break;
		}

	  }
}

void  HeapSort(int* a, int n) {
	for (int i = (n - 2) / 2; i >= 0; i--) {
		 AdjustDown(a, i, n);
	}
	int end = n - 1;
	while (end>0) {
		swap(a[0], a[end]);
		AdjustDown(a, 0, end);
		end--;
	}

}

7.复杂度分析

时间复杂度:O(N*log N)

空间复杂度:O(1)

稳定性:不稳定

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。 它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。 以此类推,直到全部待排序的数据元素的个数为零

1.算法描述

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕

2.动图展示

3.图解展示

4.代码实现

void SelectSort(int* a, int n) {
	int left = 0;
	int right = n - 1;
	int minIndex = left, maxIndex = left;
	while (left < right) {

		for (int i = left; i <= right; i++) {
			if (a[i] < a[minIndex]) {
				minIndex = i;
			}
			if (a[i] > a[maxIndex]) {
				maxIndex = i;
			}
		}

		swap(a[left], a[minIndex]);
		if (left == maxIndex) {//如果max和left位置重叠,修正一下
			maxIndex = minIndex;
		}
		swap(a[right], a[maxIndex]);
		left++;
		right--;
	}
}

5.复杂度

时间复杂度:O(N^2)

空间复杂度O(1)

稳定性:不稳定

快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

1.算法简介

从数列中挑出一个元素,称为 "基准"(pivot);

重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

2.动图展示

3.图解展示

  • 挖坑法:

挖坑法:
1、定义begin和end分别指向数据的第一个元素和最后一个元素,找一个基准值key(一般三数取中法),置array[begin]的位置上的值为基准值,并为第一个坑。
2、end从后往前走,找比key小的值,找到之后,将array[end]赋值给array[begin],填充begin位置的坑,此时end位置为一个新的

3、begin从前往后走,找比key大的值,找到之后,将array[begin]赋值给array[end],填充end位置的坑,此时begin位置为一个坑
4、此类方法依次填坑,当begin和end相遇,则循环结束,将key的值填最后一个坑。

上面这种种单趟排序的方法我们称为挖坑法

4.代码实现

int partition2(int* a, int left, int right) {
	int mid = GetMindIndex(a, left, right);//三数取中

	int key = a[left];
	swap(key, a[mid]);
	while (left < right) {
		while (left < right && a[right] >= key) {
			--right;
		}
		a[left] = a[right];
		while (left < right && a[left] <= key) {
			++left;
		}
		a[right] = a[left];

	}
	a[left] = key;
	return left;
}

三数取中代码:

int GetMindIndex(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 {
		if (a[mid] > a[right]) {
			return mid;
		}
		else if (a[left] < a[right]) {
			return left;
		}
		else {
			return right;
		}
	}
}

总代码:

int GetMindIndex(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 {
		if (a[mid] > a[right]) {
			return mid;
		}
		else if (a[left] < a[right]) {
			return left;
		}
		else {
			return right;
		}
	}
}

、

int partition2(int* a, int left, int right) {//挖坑法
	int mid = GetMindIndex(a, left, right);

	int key = a[left];
	swap(key, a[mid]);
	while (left < right) {
		while (left < right && a[right] >= key) {
			--right;
		}
		a[left] = a[right];
		while (left < right && a[left] <= key) {
			++left;
		}
		a[right] = a[left];

	}
	a[left] = key;
	return left;
}

void QuickSort(int* a, int left, int right) {//快速排序
	if (left >= right)
		return;
	int mid = partition2(a, left, right);
	QuickSort(a, left, mid - 1);
	QuickSort(a, mid + 1, right);

}

下面我们来看一下前后指针法:

  • 前后指针法:

1、选择一个基准值key,定义两个指针prev和cur(prev指向pPcur的前一个位置)
2、当cur标记的元素比key小时,prev和cur同时走,当cur标记的元素比key大时,只有cur继续向前走(此时prev停下来),当cur走到标记的元素比key值小时,cur停下,prev向前走一步,此时交换arr[cur]和arr[prev],然后,cur继续往前走。
3、当cur走出界了,将prev位置的值与key交换。

对应动图:

对应代码实现:

int partition3(int* a, int left, int right) {
	int cur = left + 1;
	int prev = left;
	int key = left;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur) {
			swap(a[cur], a[prev]);
		  }
		++cur;
	}
	swap(a[prev], a[key]);
	return prev;
}

总代码:

int GetMindIndex(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 {
		if (a[mid] > a[right]) {
			return mid;
		}
		else if (a[left] < a[right]) {
			return left;
		}
		else {
			return right;
		}
	}
}

int partition3(int* a, int left, int right) {
	int cur = left + 1;
	int prev = left;
	int key = left;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur) {
			swap(a[cur], a[prev]);
		  }
		++cur;
	}
	swap(a[prev], a[key]);
	return prev;
}

void QuickSort(int* a, int left, int right) {
	if (left >= right)
		return;
	int mid = partition2(a, left, right);
	QuickSort(a, left, mid - 1);
	QuickSort(a, mid + 1, right);

}
  • 左右指针法

1、定义两个指针begin(指向首元素)和end(指向尾元素),找一个基准值key(一般三数取中法),置于序列的第一个元素,也就是begin的位置。
2、end从后往前走找比基准值小的元素(找到后也停下),begin从前往后走找比基准值key大的元素(找到后停下),
然后,交换arr[begin]和arr[end],依次循环操作。
3、当begin与end相遇,将array[begin]或array[end]与基准值交换。

对应图解:

对应代码:

int partition(int* a, int start,int end) {
	int mid = GetMindIndex(a, start, end);
	int key = start;
	swap(a[key], a[mid]);
	while (start < end){
		while (start<end && a[end]>=a[key]) {
			--end;
		}
		while (start < end && a[start] <= a[key]) {
			++start;
		}
		swap(a[start], a[end]);
	 }
	swap(a[key], a[start]);
	return start;
}

代码汇总:

int GetMindIndex(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 {
		if (a[mid] > a[right]) {
			return mid;
		}
		else if (a[left] < a[right]) {
			return left;
		}
		else {
			return right;
		}
	}
}

int partition(int* a, int start,int end) {//左右指针法
	int mid = GetMindIndex(a, start, end);
	int key = start;
	swap(a[key], a[mid]);
	while (start < end){
		while (start<end && a[end]>=a[key]) {
			--end;
		}
		while (start < end && a[start] <= a[key]) {
			++start;
		}
		swap(a[start], a[end]);
	 }
	swap(a[key], a[start]);
	return start;
}

void QuickSort(int* a, int left, int right) {
	if (left >= right)
		return;
	int mid = partition2(a, left, right);
	QuickSort(a, left, mid - 1);
	QuickSort(a, mid + 1, right);

}

快速排序需要注意的是:

如果选取左边做key,并且使用的是左右指针法或者是挖坑法那么右边要先走才能保证结果的正确性

int partition2(int* a, int left, int right) {//挖坑法
	int mid = GetMindIndex(a, left, right);

	int key = a[left];
	swap(key, a[mid]);
	while (left < right) {
		while (left < right && a[right] >= key) {//这里一定要加等于否则有些情况会死循环
			--right;
		}
		a[left] = a[right];
		while (left < right && a[left] <= key) {//这里一定要加等于否则有些情况会死循环
			++left;
		}
		a[right] = a[left];

	}
	a[left] = key;
	return left;
}

挖坑法也是一样的要加等于如果不加等于在某些极端情况下会死循环

比如:

5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5

那么它就会死循环!!!!!!!!!!

  • 快速排序非递归

在数据结构中,即使递归能解决的,我们也有必要掌握非递归的方法。我们知道,堆空间比栈的空间大得多,当我们在栈中递归调函数,会增加栈的开销,栈帧会多。导致栈溢出程序崩溃。而在堆上递归调用函数,即使大的递归调用也不会使程序崩溃。这在安全方面就解决了栈溢出的问题。

递归就是自上而下,再自下而上。也就满足了我们数据结构中的栈。所以这里我们可以借助栈来实现非递归的快速排序。

对应代码实现:


int partition2(int* a, int left, int right) {
	int mid = GetMindIndex(a, left, right);

	int key = a[left];
	swap(key, a[mid]);
	while (left < right) {
		while (left < right && a[right] >= key) {
			--right;
		}
		a[left] = a[right];
		while (left < right && a[left] <= key) {
			++left;
		}
		a[right] = a[left];

	}
	a[left] = key;
	return left;
}

void QuickSortNonR(int* a, int n) {
	int right = n - 1;
	int left = 0;
	stack<int>ans;
	ans.push(right);
	ans.push(left);
	while (!ans.empty()) {
		int left = ans.top();
		ans.pop();
		int right = ans.top();
		ans.pop();
		int mid = partition(a, left, right);
		if (left + 1 < mid) {
			ans.push(mid - 1);
			ans.push(left);
		}
		if (mid + 1 < right) {
			ans.push(right);
			ans.push(mid + 1);
		}
	}

}

时间复杂度

归并排序:

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

1.算法分析

  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  • 重复步骤 3 直到某一指针达到序列尾;
  • 将另一序列剩下的所有元素直接复制到合并序列尾

2.动图展示

3.图解展示

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤

对应代码实现:


void _MerageSort(int* a, int* tmp , int left, int right) {
	if (left >= right)return;
	int mid = (left + right) >> 1;
	_MerageSort(a,tmp, left, mid);
	_MerageSort(a,tmp, mid + 1, right);
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int index = left;
	while (begin1 <= end1 && begin2 <= end2) {
		if (a[begin1] < a[begin2]) {
			tmp[index++] = a[begin1++];
		}
		else {
			tmp[index++] = a[begin2++];
		}
	}
	while (begin1<=end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2) {
		tmp[index++] = a[begin2++];
	}
	for (int i = left; i <= right; i++) {
		a[i] = tmp[i];
	}
 }

void MerageSort(int* a, int n) {

	int* tmp = new int[n];
	_MerageSort(a, tmp, 0, n - 1);
delete[] tmp;

}

非递归:

基本思路:看作是一颗倒过来的满二叉树,两两成对

实现思路

基本思路:

1、在State0初始状态时,两两合并,合并用到的算法是“合并有序的数组 merge sorted array”。即每次合并得到的都是有序的数组。

2、两两合并的规则是:将两个相同序列长度的序列进行合并,合并后的序列长度double。

第一趟合并(merge 1)调用了4次merge sorted array,得到了4个有序的数组:"5, 8","3, 9","6, 4","1, 4"(每个合并后的序列长度为2)

第二趟合并(merge 2)调用了2次merge sorted array,得到了2个有序的数组:"3, 5, 8, 9","1, 4, 6, 11''(每个合并后的序列长度为4)

3、按步骤1的思想以此类推,经过多次合并最终得到有序的数组,也就是State3。

可以看出经过一共3趟合并,最终得到有序的数组。

可以看出每趟要执行的合并次数不同,第一趟合并执行4次,第二趟合并执行2次,第三趟合并行1次。

看了上述的算法思想,可以知道算法可以设计成两层循环

  • 外层循环遍历趟数
  • 内层循环遍历合并次数

但是我们会发现排序元素的个数不一定是2^i

如图可知,一般数组元素的个数不一定是2i个。右边多出的"10, 7, 2"子树可以视作一般情况的情形。虽然元素个数不一定是2i个,但是任意元素个数的n,必然可以拆分成2j + m个元素的情形(数学条件不去深究)由图可知,特殊情形思想中的两两合并的规则不能满足一般情况的合并需求

  • 图中灰色的箭头代表无需合并,因为找不到配对的元素。
  • 图中墨蓝色的箭头代表两个长度不相等的元素也需要进行合

将上述的合并需求称为长短合并,容易知道长短合并仅存在1次或0次。

下面讨论的合并前/后的序列长度特指两两合并后得到的序列长度。

合并循环设计:

虽然一般情形与特殊情形的合并规则有差别(一般情形复杂),但是可以设计一个通用的合 并趟数条件。

设置变量:记录每趟合并后序列的长度,其中k是趟次

  • 通过观察发现:每次合并后序列的大小有规律,第一趟后合并的序列大小都是2,第二趟合并后的序列大小都是4,以此类推..
  • "10, 7, 2"这三个元素组合而成的序列长度虽然不满足上述的规律,但是并不影响趟数的计算。24 = 16 ≥ 11,4趟后循环结束。
  • 可以设计成:if 最后合并后的序列长度≥实际元素的个数n,这时可以说明循环结束

对应代码实现:

void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2) {
	int i = begin1;
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2) {
		if (a[begin1] < a[begin2]) {
			tmp[index++] = a[begin1++];
		}
		else {
			tmp[index++] = a[begin2++];
		}
	}
	while (begin1 <= end1) {
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2) {
		tmp[index++] = a[begin2++];
	}

	for (; i <= end2; i++) {
		a[i] = tmp[i];
	}
}
void mergeSort(int* a, int n) {
	int* tmp = new int[n];
	int gap = 1;
	while (gap < n) {

		for (int i = 0; i < n; i+=2*gap) {
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			if (begin2 >= n) {//说明已经排序完成
				break;
			}

			if (end2 >= n) {//修正区间
				end2 = n - 1;
			}

			_Merge(a, tmp, begin1, end1, begin2, end2);

		}

		gap *= 2;
	}
	delete[]tmp;
}

排序总结

如有错误请指正!!!!

到此这篇关于深入学习C语言中常见的八大排序 的文章就介绍到这了,更多相关C语言 八大排序 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入了解C语言冒泡排序优解

    目录 1:直接冒泡 2:函数冒泡 3:冒泡优化 总结: 1:直接冒泡 #include<stdio.h> int main() { int i,j; int t; int a[]={10,9,8,7,6,5,4,3,2,1};//此排序实现顺序排序 int s=sizeof(a)/sizeof(a[0]);//求数组元素个数 for(i=0;i<s-1;i++)//确定排序的趟数 { //下面为每趟冒泡排序 for(j=0;j<s-1-i;j++) { if(a[j]>a[j

  • 必须知道的C语言八大排序算法(收藏)

    概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存. 我们这里说说八大排序就是内部排序. 当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序.堆排序或归并排序序. 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短: 1.插入排序-直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到

  • C语言排序方法(冒泡,选择,插入,归并,快速)

    目录 1.冒泡排序 2.选择排序 3.插入排序 4.归并排序 5.快速排序 总结 1.冒泡排序 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序错误就把他们交换过来.走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成. 算法步骤 比较相邻的元素.如果第一个比第二个大,就交换他们两个. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对.这步做完后,最后的元素会是最大的数. 针对所有的元素重复以上的步骤,除了最后一个. 持续每次对越来越少的元素重

  • C语言每日练习之冒泡排序

    目录 分析 代码实现 运行结果 总结 分析 冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法. 冒泡排序(这里只讨论从小到大排序)可以通过二种方式实现,分别是将最小值依次移动到头部和将最大值依次移动到尾部. 代码实现 代码采用从数组头部轮询的方式: #include <stdio.h> #define INTEGER_RANGE 10 //数字范围 void bubule_sort(int *array, int len); int main() { int i =

  • C语言实现冒泡排序的思路以及过程

    目录 C语言实现<冒泡排序> 整体思路 代码实现 C语言实现<冒泡排序> 你们好!我是飞人!此篇文章是我进入IT行业第一篇博客,若有不妥之处,欢迎指点. 此篇讲解冒泡排序的原理,以及如何用C语言去实现.希望能够给各位读者带来一定的认识. 整体思路 例子:以一个整形数组为例 int arr[10]={1,2,3,4,5,6,7,8,9,10}; 我们如何进行"降序"的排序方式?? 确定躺数 总共需要排序10个数,而当我们实际去进行安排怎么去比较大小时,总共只组合了

  • C语言之快速排序案例详解

    快速排序:是对冒泡排序算法的一种改进. 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列. 例如有一个数字序列: 5 0 1 6 8 2 3 4 9 7 对其进行快速排序变为:0 1 2 3 4 5 6 7 8 9 思路如下:首先将要排序的序列的首个数字5定位比较数,这是一个参考对象! 然后的方法很简单:分别从序列的两端进行比较.先

  • C语言二叉排序树的创建,插入和删除

    目录 一.二叉排序树(二叉查找树)的概念 二.二叉排序树的判别 三.二叉排序树的创建(creat.insert) 四.二叉排序树的插入 五.二插排序树的删除 六.完整代码(可以运行) 总结 一.二叉排序树(二叉查找树)的概念 (1)若左子树非空,则左子树上所有结点的值均小于根节点的值 (2)若右子树非空,则右子树上所有结点的值均大于根节点的值 (3)左右子树分别也是一棵二叉排序树 tip:可以是一棵空树 二.二叉排序树的判别 (1)因为二叉排序树的中序遍历是一个有序递增序列,可以对已经建立的二叉

  • C语言每日练习之选择排序

    目录 分析 代码实现 总结 分析 选择排序(Selection sort)是一种简单直观的排序算法.它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾.以此类推,直到全部待排序的数据元素的个数为零.选择排序是不稳定的排序方法.--百度百科 代码实现 #include <stdio.h> #define INTEGER_RANGE 10 //数字范围 void select_so

  • 深入学习C语言中常见的八大排序

    目录 冒泡排序 1.算法描述 2.动图展示 3.图解展示 4.代码实现 5.冒泡排序的优化 6.复杂度分析 插入排序 1.算法描述 2.动图展示 3.图解展示 4.代码实现 5.复杂度分析 希尔排序 1.算法描述 2.动图展示 3.图解展示 4.代码实现 5.复杂度分析 堆排序 1.算法描述 2.动图展示 3.图解展示 4.堆的一些基本性质 5.堆的构造 6.代码实现 7.复杂度分析 选择排序 1.算法描述 2.动图展示 3.图解展示 4.代码实现 5.复杂度 快速排序 1.算法简介 2.动图展

  • 深入了解C语言中常见的文件操作方法

    目录 1.为什么使用文件 2.什么是文件 2.1文件分类 2.2 文件名 3.文件的打开和关闭 3.1文件指针 3.2 如何使用文件指针 4.文件的读写 1.为什么使用文件 大家在写程序的时候有没有一个困惑,就是我写的程序,输入一些数据后,当我把程序关掉以后数据就消失了.这是因为程序运行时,所有的数据都存储在内存中,当程序退出后,程序中的数据自然就不存在了.等下次再运行程序时,又要重新录入数据,非常难受 ​ 如何解决这个问题呢,我们可以学习使用文件来将其保存 2.什么是文件 2.1文件分类 在程

  • R语言中常见的几种创建矩阵形式总结

    矩阵概述 R语言的实质实质上是与matlab差不多的,都是以矩阵为基础的 在R语言中,矩阵(matrix)是将数据按行和列组织数据的一种数据对象,相当于二维数组,可以用于描述二维的数据.与向量相似,矩阵的每个元素都拥有相同的数据类型.通常用列来表示来自不同变量的数据,用行来表示相同的数据. R中创建矩阵的语法格式 在R语言中可以使用matrix()函数来创建矩阵,其语法格式如下: matrix(data=NA, nrow = 1, ncol = 1, byrow = FALSE, dimname

  • C语言中常见的几种流程控制语句

    目录 1.goto语句 2.if语句 3.switch语句 4.while循环 5.do...while循环 6.for循环 break和continue 总结 1.goto语句 goto语句是一种无条件转移语句,goto 语句的使用格式为: goto  语句标号; 其中语句标号是一个有效的标识符,这个标识符加上一个 ":" 一起出现在函数内某处,执行goto语句后,程序将跳转到该标号处并执行其后的语句: 另外语句标号必须与goto语句同处于一个函数中,但可以不在一个循环层中:通常go

  • GO语言中常见的排序算法使用示例

    目录 快排 冒泡 选择排序 插入排序 希尔排序 二分法查找 快排 package main import ( "fmt" "math/rand" "time" ) func main() { li:=[]int{1,3,5,2,4,6,9,7} left:=0 right:=len(li)-1 fmt.Println(quick_sort(li,left,right)) } func quick_sort(li []int, left,right

  • python列表中常见的一些排序方法

    目录 1.冒泡排序法 方法一:直接使用for循环 方法二:使用while语句 2.选择排序法 方法一:remove和append同时使用 方法二:pop和append同时使用 3.list.sort()方法 4.sorted()函数 总结 1.冒泡排序法 让列表中的一项和下一项作比较,若前一项大于后一项则交换两者位置(升序). 方法一:直接使用for循环 L=[8,2,50,3] for i in range(len(L)): for j in range(i+1,len(L)): if L[i

  • 深入学习C语言中的函数指针和左右法则

    通常的函数调用     一个通常的函数调用的例子: //自行包含头文件 void MyFun(int x); //此处的申明也可写成:void MyFun( int ); int main(int argc, char* argv[]) { MyFun(10); //这里是调用MyFun(10);函数 return 0; } void MyFun(int x) //这里定义一个MyFun函数 { printf("%d\n",x); } 这个MyFun函数是一个无返回值的函数,它并不完成

  • C语言中的常量详解

    目录 C语言中的常量 字面常量 #define定义的标识符常量 枚举常量 C语言中的常量 C编程中的常量是一些固定的值,它在整个程序运行过程中无法被改变. 字面常量 字面常量是直接写出的固定值,它包含C语言中可用的数据类型,可分为整型常量,字符常量等.如:9.9,"hello"等就属于这一类常量. ##const修饰的常变量 有的时候我们希望定义这么一种变量:值不能被修改,在整个作用域中都维持原值.为了满足用户需求,C语言标准提供了const关键字.在定义变量的同时,在变量名之前加上c

  • ASP.NET学习中常见错误总结归纳

    目录 前言 下拉框绑值 绑值GridView 删除数据 修改 修改赋值到另外一个页面 修改赋值到另外一个页面绑定值 换页不报错 前言 自己在学习.NET中常犯的错误(持续更新) 下拉框绑值 public void ddlist() { this.DropDownList1.DataTextField = "DeviceName"; this.DropDownList1.DataValueField = "DeviceID"; this.DropDownList1.D

  • C语言二叉树常见操作详解【前序,中序,后序,层次遍历及非递归查找,统计个数,比较,求深度】

    本文实例讲述了C语言二叉树常见操作.分享给大家供大家参考,具体如下: 一.基本概念 每个结点最多有两棵子树,左子树和右子树,次序不可以颠倒. 性质: 1.非空二叉树的第n层上至多有2^(n-1)个元素. 2.深度为h的二叉树至多有2^h-1个结点. 满二叉树:所有终端都在同一层次,且非终端结点的度数为2. 在满二叉树中若其深度为h,则其所包含的结点数必为2^h-1. 完全二叉树:除了最大的层次即成为一颗满二叉树且层次最大那层所有的结点均向左靠齐,即集中在左面的位置上,不能有空位置. 对于完全二叉

随机推荐