总结:八大排序算法(图解+代码)

一、直接插入排序

基本思想:

     把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程.

图解:

这里写图片描述

代码实现:
void InsertSort(int *a, int size)
{
	assert(a);
	for (int i = 0; i < size - 1; i++)
	{
		int end = i; //有序表最后一个下标
		int key = a[end + 1]; //待插入元素
		while (end >= 0 && key < a[end]) //查找合适位置
		{
			a[end + 1] = a[end];
			end--;
		}
		a[end + 1] = key; //插入
	}
}
分析:

时间复杂度:

  • 最好情况:
    如果待排序的元素本身有序,那么在进行插入排序时,每一个元素直接在前面有序表末尾处进行插入,整个过程下来,时间复杂度为O(N)
  • 最坏情况:
    如果待排序的元素无序,那么在进行插入排序时,每一个元素都需要在前面的有序表中找到其合适的插入位置,整个过程下来,时间复杂度为O(N^2)
  • 平均情况:O(N^2)

空间复杂度:O(1)
稳定性:稳定
说明:设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面,在排序之后,a[i]仍然在a[j]前面,则这个排序算法是稳定的。

二、希尔排序

基本思想:

(1)预排序:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成),分别进行直接插入排序,然后依次缩减增量再进行排序,使整个序列接近有序
(2)当整个序列中的元素基本有序时,再对全体元素进行一次直接插入排序
     因为直接插入排序在元素基本有序的情况下效率是很高的,因此希尔排序在时间效率上相对于直接插入排序有较大提高。

图解:

这里写图片描述

代码实现:
void ShellSort(int *a, int size)
{
	assert(a);
	int gap = size;
	while (gap > 1)
	{
		gap = gap / 3 + 1; //增量(步长)
		for (int i = 0; i < size - gap; i++)
		{
			int end = i; //有序表最后一个下标
			int tmp = a[end + gap]; //待插入元素
			while (end >= 0 && tmp < a[end]) //查找合适位置
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			a[end + gap] = tmp; //插入
		}
	}
}
分析:

时间复杂度:

  • 最好情况:O(N)
  • 最坏情况:O(N^2)
  • 平均情况:O(N^1.3)

空间复杂度:O(1)
稳定性:不稳定
     例如:待排序列3 2 2* 4,当gap为2时进行希尔排序,经过排序后变为2* 2 3 4,此时2和2*之间的相对位置发生变化。

三、直接选择排序

基本思想:

     在元素序列a[i] ~ a[n-1]中选择关键码最大(最小)的数据元素,将它与这组元素中的最后一个(第一个)元素进行交换,接着在剩余的元素序列a[i] ~ a[n-2](a[i+1] ~ a[n-1])中重复上述步骤,直到剩余1个元素时完成排序。

图解:

这里写图片描述

代码实现:
void SelectSort(int *a, int size)
{
	assert(a);
	int left = 0;
	int right = size - 1;
	while (left < right)
	{
		int min = left;
		int max = left;
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[min]) //选出最小
			{
				min = i;
			}
			if (a[i] > a[max]) //选出最大
			{
				max = i;
			}
		}
		Swap(&a[left], &a[min]);
		if (max == left) //若max和left相等,则经过上一步交换,导致原max处为最小值,而min处为最大值
		{
			max = min; //更新max,让其位置为最大值
		}
		Swap(&a[right], &a[max]);
 		left++;
		right--;
	}
}
分析:

时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
     例如上述图解,排序前 25 在 25* 之前,而在排序后 25 在 25* 后。

四、堆排序

基本思想:

     升序建大堆,降序建小堆
     以升序为例,先将整个序列的元素建造成一个大堆,接着把堆顶元素和当前堆的最后一个元素进行交换,然后堆元素个数减1,接着从根节点通过向下调整使得当前堆恢复到大堆,重复上述过程,直到当前堆的元素个数为1时完成排序。

图解:

这里写图片描述
这里写图片描述

代码实现:
//自顶向下调整
void AdjustDown(int *a, int size, int root)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < size)
	{
		int flag = 0;
		if (child + 1 < size)
		{
			if (a[child + 1] > a[child])
			{
				child++;
			}
		}
		if (a[child]>a[parent])
		{
			flag = 1;
			Swap(&a[child], &a[parent]);
		}
		if (flag == 0) //优化
		{
			break;
		}
		parent = child;
		child = parent * 2 + 1;
	}
}

void HeapSort(int *a, int size)
{
	assert(a);
	int i = (size - 2) / 2;
	for (; i >= 0; i--) //先建大堆
	{
		AdjustDown(a, size, i);
	}

	for (i = size - 1; i > 0; i--) //排序
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);
	}	
}
分析:

时间复杂度:O(N*lgN)
空间复杂度:O(1)
稳定性:不稳定

五、冒泡排序

基本思想:

     从元素序列第一个位置开始,进行两两比较,根据大小交换位置,直到最后将最大(最小)的数据元素交换到了当前序列的最后一个位置,成为有序序列的一部分,然后元素个数减1,重复上述过程,直到所有数据元素都排好序。

图解:

这里写图片描述

代码实现:
void BubbleSort(int *a, int size)
{
	assert(a);
	for (int i = 0; i < size - 1; i++)
	{
		int flag = 0;
		for (int j = 0; j < size - i - 1; j++) //一趟排序
		{
			if (a[j]>a[j + 1])
			{
				flag = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (flag == 0) //如果一趟排序后发现没有一次交换,则说明已经有序
		{
			break;
		}
	}
}
分析:

时间复杂度:

  • 最好情况:O(N)
  • 最坏情况:O(N^2)
  • 平均情况:O(N^2)

空间复杂度:O(1)
稳定性:稳定

六、快速排序

基本思想:

     任取待排列元素序列中的一个元素作为基准值,通过一趟排序将要排序的序列分割成独立的两子序列,其中左子序列的所有元素都比基准值小,右子序列的所有元素都比基准值大,然后左右子序列重复此过程,直到所有元素都排列在相应的位置上为止。

图解:

这里写图片描述

将区间按照基准值划分成左右两部分的方法有:

(1)左右指针法:
     定义两个指针begin和end,将基准值放在最右边,begin从头开始找比基准值大的值(begin++),end从尾开始找比基准值小的值(end–),若都找到且begin小于end,则两者值交换,重复上述过程,直到begin>=end时,将begin所对应的值和最右边的基准值交换,此时整个序列被基准值划分成左右两个子序列。
这里写图片描述

int PartSort1(int *a, int left, int right)
{
	int index = GetMid(a, left, right); //三数取中,选取基准值
	Swap(&a[index], &a[right]); //将基准值和最右边的值交换
	int key = a[right];
	int begin = left;
	int end = right;
	while (begin < end)
	{
		//begin选大
		while (begin < end && a[begin] <= key) //"="可省略
		{
			begin++;
		}
		//end选小
		while (begin < end && a[end] >= key) //"="不可省略
		{
			end--;
		}
		if (begin < end)
		{
			Swap(&a[begin], &a[end]);
		}
	}
	Swap(&a[begin], &a[right]);
	return begin;
}

(2)挖坑法:
     定义两个指针begin和end,将基准值放在最右边并保存该值,此时该位置可视为一个坑。begin从头开始找比基准值大的值,找到后将begin所对应的值填入到刚才的坑中,此时begin这个位置成为新的坑;begin不动,接着end从尾开始找比基准值小的值,找到后将end所对应的值填入到刚才的新坑中,此时end这个位置成为新的坑;end不动,begin从上次的位置接着往后找,重复上述过程,直到begin>=end时,将保存的基准值填入到begin所对应的坑中,此时整个序列被基准值划分成左右两个子序列。
这里写图片描述

int PartSort2(int *a, int left, int right)
{
	int index = GetMid(a, left, right); //三数取中,选取基准值
	Swap(&a[index], &a[right]); //将基准值和最右边的值交换
	int key = a[right]; //保存基准值
	int begin = left;
	int end = right;
	while (begin < end)
	{
		//begin选大
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[end] = a[begin];
		//end选小
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[begin] = a[end];
	}
	a[begin] = key;
	return begin;
}

(3)前后指针法:
     定义两个指针prev和cur,将基准值放在最右边,prev初始位置在left-1处,cur初始位置在left处。cur从头开始找比基准值小的值,找到后若此时++prev的位置和cur的位置不在同一处(说明++prev对应的值一定比基准值大),则交换这两处的值,cur接着刚才的位置往后找,重复上述过程,直到cur>=right时,将最右边的基准值和++prev所对应的值进行交换,此时整个序列被基准值划分成左右两个子序列。
这里写图片描述

int PartSort3(int *a, int left, int right) 
{
	int index = GetMid(a, left, right); //三数取中,选取基准值
	Swap(&a[index], &a[right]); //将基准值和最右边的值交换
	int prev = left - 1;
	int cur = left;
	while (cur < right)
	{
		if (a[cur] < a[right] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[++prev], &a[right]);
	return prev;
}
三数取中法:

     在选择基准值时,为了提高排序效率,我们常常利用三数取中法来选择基准值,所谓的三数指的是:序列最左端的值、中间位置的值和最右端的值,计算它们的中位数来作为基准值。

int GetMid(int *a, int left, int right)
{
	int mid = (left + right) >> 1;
	if (a[left] < a[right])
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[mid] < a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}
代码实现:

(1)递归法:

void QuickSortR(int *a, int left, int right)
{
	assert(a);
	if (left >= right)
	{
		return;
	}

	if (right - left < 10) //优化:在区间较小时,插入排序的效率高
	{
		InsertSort(a, right - left + 1);
	}
	else
	{
		int div = PartSort1(a, left, right);
		QuickSortR(a, left, div - 1);
		QuickSortR(a, div + 1, right);
	}
}

(2)非递归法:
     借用栈的结构来模仿递归(相关栈的函数定义请查看顺序栈

void QuickSort(int *a, int left, int right)
{
	assert(a);
	Stack s;
	StackInit(&s);
	StackPush(&s, left);
	StackPush(&s, right);
	while (!StackEmpty(&s))
	{
		int end = StackTop(&s);
		StackPop(&s);
		int begin = StackTop(&s);
		StackPop(&s);
		int div = PartSort1(a, begin, end);
		if (begin < div - 1)
		{
			StackPush(&s, begin);
			StackPush(&s, div - 1);
		}
		if (div + 1 < end)
		{
			StackPush(&s, div + 1);
			StackPush(&s, end);
		}
	}
}
分析:

时间复杂度:

  • 最好情况:O(N*lgN)
  • 最坏情况:O(N^2)
  • 平均情况:O(N*lgN)

空间复杂度:O(lgN)——递归深度
稳定性:不稳定

七、归并排序

基本思想:

     将待排序的元素序列分成两个长度相等的子序列,对每一个子序列排序,然后再将它们合并成一个有序序列。

图解:

这里写图片描述

合并图解:

这里写图片描述

代码实现:
//归并局部递归
void _MergeSort(int *a, int left, int right, int *tmp)
{
	if (left >= right)
	{
		return;
	}

	if (right - left < 10) //优化:在区间较小时,插入排序的效率高
	{
		InsertSort(a, right - left + 1);
		return;
	}

	int mid = left + ((right - left) >> 1);
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int p = left;
	int q = mid + 1;
	int index = 0;
	while (p <= mid&&q <= right)
	{
		if (a[p] <= a[q]) //加上"="可保持稳定性
		{
			tmp[index++] = a[p++];
		}
		else
		{
			tmp[index++] = a[q++];
		}
	}
	while (p <= mid)
	{
		tmp[index++] = a[p++];
	}
	while (q <= right)
	{
		tmp[index++] = a[q++];
	}

	int j = 0;
	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[j++];
	}
}

void MergeSort(int *a, int left, int right)
{
	assert(a);
	int *tmp = (int*)malloc((right - left + 1)*sizeof(int));
	memset(tmp, 0, (right - left + 1)*sizeof(int));
	//或者:int *tmp = (int *)calloc(right - left + 1,sizeof(int));
	_MergeSort(a, left, right, tmp);
	free(tmp);
}
分析:

时间复杂度:O(N*lgN)
空间复杂度:O(N)——临时数组
稳定性:稳定

八、计数排序

基本思想:

     统计待排序序列中每个元素出现的次数,再根据统计的结果重新对元素进行回收。

图解:

这里写图片描述

代码实现:
void CountSort(int *a, int size)
{
	assert(a);
	//(1)找出最值,确定范围,以便开辟空间
	int max = a[0];
	int min = a[0];
	int index = 0;
	for (index = 1; index < size; index++)
	{
		if (a[index]>max)
		{
			max = a[index];
		}
		if (a[index] < min)
		{
			min = a[index];
		}
	}
	int range= max - min + 1;
	//(2)统计出现次数
	int *tmp = (int*)calloc(range, sizeof(int));
	for (index = 0; index < size; index++)
	{
		tmp[a[index] - min]++;
	}
	//(3)回收
	int i = 0;
	for (index = 0; index < range; index++)
	{
		while (tmp[index])
		{
			a[i++] = index + min;
			tmp[index]--;
		}
	}
	free(tmp);
	tmp = NULL;
}
分析:

时间复杂度:O(N+range)
空间复杂度:O(range)
稳定性:稳定

九、总结:

这里写图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章