带你手撕排序算法

文章目录

  • 前言
  • 一、排序的相关概念
  • 二、插入排序
    • 1.直接插入
      • (1)算法思想
      • (2)算法讲解
      • (3)代码及解析
      • (4)代码总结
    • 2.希尔排序( 缩小增量排序 )
      • (1)算法思想
      • (2)算法讲解
      • (3)代码及其解析
      • (4)代码总结
  • 三、选择排序(改进版)
    • 1.直接选择排序
      • (1)算法思想
      • (2)算法讲解
      • (3)算法注意事项
      • (4)代码及解析
      • (5)算法总结
    • 2.堆排序
      • (1)堆的介绍
      • (2)算法思想
      • (3)算法讲解
      • (4)代码及解析
      • (5)时间复杂度讲解
      • (6)算法总结
  • 三、交换排序
    • 1.冒泡排序
      • (1)算法思想
      • (2)算法讲解
      • (3)算法代码
      • (4)算法总结
    • 2.快速排序
      • (1)算法思想
      • (2)挖坑法
      • (3)左右指针法(不建议使用)
      • (4)前后指针法
      • (5)总代码及解析
      • (6)注意事项及改进
      • (7)算法总结
  • 三、归并排序
    • 1.归并排序
      • (1)算法思想
      • (2)算法讲解
      • (3)代码及解析
      • (4)算法总结
  • 总结


带你手撕排序算法_第1张图片

前言

鸽了好久了,希望大家能够理解,排序算法的学习和码文确实挺费时间的,本篇文章重要讲解以下排序算法和其拓展,其他排序以后会补充


带你手撕排序算法_第2张图片

一、排序的相关概念

概念提前说明一下,方便大家理解后续文章

  • 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起
    来的操作
  • 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记
    录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍
    在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的
  • 内部排序:数据元素全部放在内存中的排序
  • 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据
    的排序

二、插入排序

1.直接插入

(1)算法思想

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

带你手撕排序算法_第3张图片

(2)算法讲解

带你手撕排序算法_第4张图片

我们这里设排好了的数组的最后一位的位置为end,设temp = a[end + 1]

带你手撕排序算法_第5张图片

end+1=2, 2向前找比它小的数,如果没找到或者已经到头了,就将比他大的数全部为后面移动一位,所以这里5位后面移动一位,然后将已经保存在temp的2赋给啊a[end+1]

可能有人说这种情况不典型,好再讲一个比较典型的
带你手撕排序算法_第6张图片

这里3向前面找比他小的,然后他找到了2,在没找到之前,讲4,5,6向移动一位,在找到后,将3赋给找到的数的前面一位

以下为完整过程
带你手撕排序算法_第7张图片

(3)代码及解析

void InsertSort(int* a, int m)
{
	int i;
	for (i = 0; i < m - 1; ++i)
	{
		int end = i;
		int temp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > temp)//这一步是将大于temp的数全部向后移动
			{
				a[end + 1] = a[end];
				end--;
			}
			else//找到比temp小的,就直接退出			
			{
				break;
			}	
		}
		a[end + 1] = temp;
	}
}

(4)代码总结

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:最坏情况为逆序时,或者接近逆序时O(N^2),最好情况为升序时,或者接近升序时O(N)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

当接近有序时,我们将比temp大的数移动的次数将会减少,所以算法的效率更高,因此我们就需要对原本的数据进行处理,使得效率得以提升,那就是希尔排序

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

(1)算法思想

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

(2)算法讲解

这里放一张图,基本就清楚了

我们对其进行分组,每组都用直接插入排序进行排序,那么整体就接近有序了,当gap=1时就是直接插入排序了
带你手撕排序算法_第8张图片

(3)代码及其解析

可能有细心的小伙伴就发现了,无非就是将插入排序的1改成了gap吗?也确实是这样,无非是在插入排序上加了一个while循环控制gap而已

void ShellSort(int* a, int m)
{
	int gap = m;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//这里加一就可以保证gap最后为1,从而变成直接插入排序
		for (int i = 0; i < m - gap; i++)
		{
			int end = i;
			int temp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > temp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = temp;
		}
	}
}

(4)代码总结

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试对比。
  3. 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3—N2)
  4. 稳定性:不稳定

三、选择排序(改进版)

1.直接选择排序

(1)算法思想

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

(2)算法讲解

maxi向前面找最大的数,maxi记录最大的数的下标,mini向前找最小的数,mini记录最小的数的下标,然后将min与begin位置的值交换,将max与end位置的值交换
带你手撕排序算法_第9张图片

(3)算法注意事项

如果在maxi交换或者mini的交换过程中交换的值刚好出现在begin或者end,那么就会出现bug,bug有以下处理方法

带你手撕排序算法_第10张图片
你问我为什么手写?哈哈哈,手写的更加引人瞩目一点,你要是说我字丑,我就打洗你(

(4)代码及解析

void SelectSort(int* a, int m)
{
	int begin = 0;
	int end = m - 1;
	while (begin < end)
	{
		int maxi = begin;
		int mini = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
			if (a[mini] > a[i])
			{
				mini = i;
			}
		}
		Swap(&a[end], &a[maxi]);
		if (mini == end)//这里不懂看注意事项
		{
			mini = maxi;
		}
		Swap(&a[begin], &a[mini]);

		begin++;
		end--;
	}
}

(5)算法总结

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:传统O(N^2),改进的代码能到N*logN
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

2.堆排序

堆排序就需要对前面的二叉树有一定的理解才能弄明白,如果不懂的可以移步至我的前一篇文章

(1)堆的介绍

一张图解决

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

(2)算法思想

堆排序主要用的是向下调整算法·,向下调整算法建立在左右子树已经是大堆或者是小堆才能有用,那么向下调整的思想是怎么实现的?
如果我们是建大堆,向下调整算法是将一个节点与其子节点的最大值比较比较,如果子节点有比其大的,就交换两者的值,然后继续向下,将交换的节点的子节点继续向下比直到叶子节点(此算法有一个大前提),就是子节点必须是大堆或者小堆

那么就有人问如果子节点不是大堆或者小堆怎么办?我们可以从最底部不是叶子节点的位置开始(因为叶子节点本身就身就是大堆或者小堆),对每个每个节点都使用向下调整算法,一直调到root节点,那么一个堆就完成了,然后最大的值放在最后,然后继续对这个最大的数的前面的数建堆,然后继续与未排序好的数组的交换将是不是就ok了呢?这就是堆排序的思想,总是搞出最大的数排在最后

(3)算法讲解

我们拿这个二叉树讲
带你手撕排序算法_第12张图片
这里我们建大堆,我们首先找到最后一个非叶子节点8,然后用8比较其节点,无需交换在到7这个节点,其应该与9交换,然后就到2,其应该与6交换(这是建堆的过程,后面的交换过程我就不讲了)
带你手撕排序算法_第13张图片
然后看这图
带你手撕排序算法_第14张图片

(4)代码及解析

//向下调整代码(父子关系不清楚的看堆的介绍)
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//堆排序算法
void HeapSort(int* a, int n)
{
	
	int i;
	int end = n - 1;
	//建堆,将一个数组建成一个堆的样子
	for (i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
	//交换堆的root到最后,然后将数组的大小减小,然后继续向下用调整算法
	while (end > 0)
	{
		Swap(a, &a[end]);
		end--;
		AdjustDown(a, end + 1, 0);//因为这个数组已经是一个堆了所以我们只需要对其用向下调整算法就行了
	}

}

(5)时间复杂度讲解

这个算法的时间复杂度不好求解,所以我来讲解一下

有人问为什么用图?因为图清晰明了(忽略字,说字丑的小心我打洗你)
求堆排序的复杂度需要知道高中的数列的错位相减法(自行了解)
带你手撕排序算法_第15张图片

(6)算法总结

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:向下调整算法O(N)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

三、交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.冒泡排序

(1)算法思想

与水中的气泡上浮相似,大的先上去,小的后上去

(2)算法讲解

一图解决
带你手撕排序算法_第16张图片

(3)算法代码

这里偷下懒,更加推荐各位去看我前面的文章,这里对冒泡排序有更加深刻的解释冒泡排序改进

(4)算法总结

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

2.快速排序

(1)算法思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序
元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有
元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所
有元素都排列在相应位置上为止
带你手撕排序算法_第17张图片
带你手撕排序算法_第18张图片

(2)挖坑法

先设立一个key记录begin的值,再在begin处设个坑pivot,先让end从后往前找比key小的值,找到了就将这个值放入坑中,然后这个比key小的值就成为了坑,然后用begin向后找比key大的值找到了就放入坑中,重复上述过程最后begin和end相交,这个位置就是key应该到的位置(代码中的三数取中后面的改进方法会讲)

//挖坑法
int PartSort1(int* a, int left, int right)
{

	int begin = left, end = right;
	int mid = GetMidIndex(a, left, right);//三数取中
	Swap(&a[mid], &a[begin]);
	int key = a[begin];
	int pivot = begin;
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[pivot] = a[end];
		pivot = end;
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;
	}
	a[begin] = key;
	return pivot;
}

(3)左右指针法(不建议使用)

先设立一个keyi用来记录begin,用begin向后找比keyi大的值,找到了就停下,让end从后往前找比key小的值找到了就停下,然后交换两者的值,两者相遇的位置即是keyi的位置,然后交换keyi位置的值和相遇位置的值

//左右指针法
int PartSort2(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	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[keyi], &a[begin]);
	return begin;
}

(4)前后指针法

keyi记录begin,设prev = left, cur = left + 1,让cur往后面找比key小的值,找到了就给prev++,然后给交换prev和cur的值,一直重复直到cur>=n,prev的位置就应该放key,下面是一个例子的过程
带你手撕排序算法_第19张图片

(5)总代码及解析

//三数取中
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 begin = left, end = right;
	int mid = GetMidIndex(a, left, right);//三数取中
	Swap(&a[mid], &a[begin]);
	int key = a[begin];
	int pivot = begin;
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[pivot] = a[end];
		pivot = end;
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;
	}
	a[begin] = key;
	return pivot;
}
//左右指针法
int PartSort2(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	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[keyi], &a[begin]);
	return begin;
}
//前后指针法
int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int prev = left, cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		while (a[cur] < a[keyi] && ++prev != cur)//自己交换自己浪费资源++prev != cur是减少不必要的浪费
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyindex = PartSort3(a, left, right);//可以调用3种方法的任意一种
	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);
	}
}

(6)注意事项及改进

看到这里,有人可能有两个疑问
1.三数取中是什么,为什么要用三数取中?
2.快排主代码中为什么有插入排序?

>1.三数取中是将头和尾还有中间值这3个数中取一个·不大不小的值来当key,可以有效地防止出现数据有序(升序和降序)的最坏的情况
>因为一旦有序,每一次都相当于遍历了,效率很低
>2.这快排主要是依靠递归实现的,一旦区间分的很小后,递归深度会很深,所以被分成较小的区间时,数据比较有序,这时用插入排序效率更好

(7)算法总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

三、归并排序

1.归并排序

(1)算法思想

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

(2)算法讲解

两图解决,当们将一组数据分为两半,用begin1和begin2分别指向两边的数据的首元素,依次比较,将小的放到另外的一个数组,在任意一个数组被拿完后,将另外一个数组全部复制过去,如果这个数组两边是有序的,我们就完成了,可惜两边不是有序的,那我们就继续分,直到分成只有一个,一个数据他始终有序,然后将两个一个的数组合在一起归并,这两个数据就有序了,然后这两个和另外的合并,直到完全合并,就行了
带你手撕排序算法_第20张图片
带你手撕排序算法_第21张图片

(3)代码及解析

void _MergeSort(int* a, int left, int right, int* temp)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) >> 1;
	_MergeSort(a, left, mid, temp);
	_MergeSort(a, mid + 1, right, temp);
	//这里用二叉树的知识理解,左右节点就是一层,当我我们解决掉一层后,我们就需要对这个代码进行归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[index++] = a[begin1++];
		}
		else
		{
			temp[index++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		temp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[index++] = a[begin2++];
	}
	for (int i = left; i <= right; i++)
	{
		a[i] = temp[i];
	}
}

void MergeSort(int* a, int m)
{
	int* temp = (int*)malloc(sizeof(int) * m);
	_MergeSort(a, 0, m - 1, temp);
	free(temp);
}

(4)算法总结

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

总结

带你手撕排序算法_第22张图片
好了,这篇文章总算爆肝完了(哭),希望对你有帮助,有错误请指正,谢谢

你可能感兴趣的:(数据结构,c语言,排序算法,算法,数据结构)