这几天学习了直接插入排序,希尔排序,和选择排序。写这篇博客复习一下。

直接插入排序

直接插入排序的时间复杂度为o(n^2),其中n是待排序元素的数量。最坏情况下,数组完全无序,每个元素都需要与之前已排序的所有元素比较一次,时间复杂度为o(n^2)。但是在最好情况下,数组已经完全有序,每个元素只需要与前一个元素比较一次即可,时间复杂度为o(n)。平均时间复杂度为o(n^2)。

直接插入排序其实就和打牌时摸牌一样,假设我现在摸了567三张牌,然后我们再次摸了一次牌,这次牌为3,然后我会将3和7比较7大于三则3在7之间,再和6比较6大所以3在6前面,最后和5比较,5大所以3在5的前面,因为5的前面已经没有元素了,所以这里就将3放到5的前面。然后我们再一次摸下一张牌,直到最后我将所有的牌都摸完。

那么总结一下那就是将待插入的数据和前面已经有序的数组比较,找到待插入数据在有序数组里面的位置,将其放入。那么这一个过程也就是直接插入排序。

下面是代码:

void InsertSort(int* arr, int n)//这里的第一个参数是待比较数组,第二个参数是数组的元素个数
{
	for (int i = 1; i < n; i++)
	{
		//我们先来看一趟排序,即现在下标为0到end的元素为有序区元素,而tmp则为我们要插入的元素
		int end = i-1;
		int tmp = arr[i];
		//下面我们要遍历有序区间区寻找位置
		while (end>=0)//这里的end必须等于0因为当有序区只有一个元素的时候,也要进行一次判断
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];//如果正在比较的这个有序区的元素比待插入元素大
				//则将这个有序区元素往后移动
				end--;//比较玩一个元素自然要去寻找下一个
			}
			else
			{
				break;//如果要比较的这个有序区的元素比待比较的这个元素小则要将待插入元素放到这个元素的后面
			}
		}
		arr[end + 1] = tmp;//由此我们就完成了将一个元素放到有序区间,现在我们要将一个数组排序成功,那么肯定要将第一个元素默认为有序元素
	}
}

下面是测试代码

void testinsertsort()
{
	int arr[] = { 1,4,7,8,5,2,9,6,3,10 };//先写一个无序的数组
	int n = sizeof(arr) / sizeof(arr[0]);//得到数组的元素有多少个
	printf_Arr(arr, n);//这是我写的一个打印数组的函数,因为只用一个for循环就可以解决,所以这里就
  //不展示了
	InsertSort(arr, n);
	printf_Arr(arr, n);
}

希尔排序

那么在什么时候直接插入排序计算的次数最少呢?那肯定是当数组里面大部分都有序的时候,那么希尔排序也就是这样。

希尔排序主要有两个步骤。

希尔排序步骤一:预排序

第一个步骤:将待排序的数组分为不同的组,并对每一个组都使用直接插入排序算法。

经过这第一个步骤那么数组也就变成了接近有序的情况。

那么怎么将数组分成不同的组呢?那么这里就要通过希尔增量来帮忙了。希尔增量是一个常数,那么就可以通过希尔增量来实现分组下面我画图来解释。

详解希尔排序,直接插入排序,选择排序(c实现)_数组

那么下面就对红色组进行直接插入排序

下面是代码实现:

void Shellsort(int* arr, int n)
{
	int gap = 3;//这里的gap也就是希尔增量
	//首先我们只写对一组的直接插入排序
	for (int i = 0; i < n; i += gap)
	{
		int tmp = arr[i];//tmp依旧保存待插入元素
		int end = i - gap;//在一组内你知道一组里面的第二个元素的下标为i上一个点的下标自然是i-gap
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + gap] = arr[end];
				end -= gap;//这两个的理由依旧和上面的一样我们要将一个组内的元素进行直接插入排序
			}
			else
			{
				break;
			}
		}
		arr[end + gap] = tmp;//如果找到了tmp在组内所在的正确位置,则将tmp放到哪一个位置上
	}
}

运行截图:

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_02

可以很明确的看到963那一组已经有序了。那么怎么去让所有组都进行一次排序呢?解决方法有两个,

第一个再增加一个for循环,用于控制开始的下标开始的下标不同我们就能进入不同的组,开始下标为0我们进入的就是红色组,下标为1我们进入的就是绿色组,下标为2我们进入的就是粉色组。

下面是代码实现:

void Shellsort(int* arr, int n)
{
	int gap = 3;//这里的gap也就是希尔增量
	//首先我们只写对一组的直接插入排序
	for (int j = 0; j < gap; j++)//用于控制开始的下标,用于进入不同的组
	{
		for (int i = j; i < n; i += gap)
		{
			int tmp = arr[i];//tmp依旧保存待插入元素
			int end = i - gap;//在一组内你知道一组里面的第二个元素的下标为i上一个点的下标自然是i-gap
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;//这两个的理由依旧和上面的一样我们要将一个组内的元素进行直接插入排序
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;//如果找到了tmp在组内所在的正确位置,则将tmp放到哪一个位置上
		}
	}
}

下面是代码运行截图:

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_03

可以看到更为接近有序了。

那么实现对不用组的希尔排序还有一个代码

void Shellsort(int* arr, int n)
{
	int gap = 3;//这里的gap也就是希尔增量
	//首先我们只写对一组的直接插入排序
		for (int i = 0; i < n; i ++)
		{
			int tmp = arr[i];//tmp依旧保存待插入元素
			int end = i - gap;//在一组内你知道一组里面的第二个元素的下标为i上一个点的下标自然是i-gap
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;//这两个的理由依旧和上面的一样我们要将一个组内的元素进行直接插入排序
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;//如果找到了tmp在组内所在的正确位置,则将tmp放到哪一个位置上
		}
}

这两种方法除了思想不同外,运行效率差不多。

第一种方法是一组一组的进行,也即先将红色组排序完成,再对其它组进行排序。

第二种方法是多组并行,也即第一次下标为0对红色组的前0个元素比较,当下标为1的时候又对绿组进行前0个元素的比较。

可以知道的是在希尔增量为1的时候就是直接插入排序一定可以将数组排列成有序。这里也就引出了希尔排序的第二步骤。

希尔排序步骤二:修改希尔增量

这里有两种希尔增量的修改方法。

方法一:每一次都让gap/3+1,那么当gap小于3的时候前面算出0,再加一个1自然让gap变为1

void Shellsort(int* arr, int n)
{
	int gap = n;//这里的gap也就是希尔增量
	while (gap > 1)//这一个循环也就是对希尔增量进行改变,这里绝对不能等于1不然会死循环
  //因为当gap为2的时候进入循环,下面就会将gap修改为1,如果等于1,那么在gap为1的时候进入
  //1/3为0在加一个1再次变成1,进入死循环。
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n; i++)
		{
			int tmp = arr[i];//tmp依旧保存待插入元素
			int end = i - gap;//在一组内你知道一组里面的第二个元素的下标为i上一个点的下标自然是i-gap
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;//这两个的理由依旧和上面的一样我们要将一个组内的元素进行直接插入排序
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;//如果找到了tmp在组内所在的正确位置,则将tmp放到哪一个位置上
		}
	}
}

方法二:也就是让gap每一次都除以2,那么在这种情况下就不用+1了。因为无论是奇数还是偶数不断除以2,都会出现一个1。

下面是运行截图:

详解希尔排序,直接插入排序,选择排序(c实现)_数组_04

选择排序

选择排序的思路很简单,从数组里面找到最小或是最大的数,将最小或是最大的数放到数组头/数组尾,然后去寻找次大的数/次小的数,直到将整个数组遍历完成。

我这里修改一下,我直接寻找最大和最小,然后将最大和最小放到头和尾,再去寻找次大和次小。

下面是代码实现:

void Selectsort(int* arr, int n)
{
	int begin = 0;//数组头
	int end = n - 1;//数组尾
	while (begin < end)//当begin和end相等的时候代表数组已经寻找完毕了
	{
		int maxi = begin;//用于储存最大值的下标
		int mini = begin;//用于储存最小值的下标
		for (int i = begin; i <= end; i++)
		{
			if (arr[i] < arr[mini])//如果发现任何一个值小于此时的最小值那就更新这个值
			{
				mini = i;
			}
			if (arr[i] > arr[maxi])
			{
				maxi = i;//同理更新maxi
			}
		}
		//下面我们就要交换了
		swap(&arr[begin], &arr[mini]);
		if (maxi == begin)//这里需要处理一下当最大的值在begin的位置的时候我们就要修改
    //一下maxi的下标因为经过交换之后begin的元素就被交换到了mini的位置
		{
			maxi = mini;
		}
		swap(&arr[end], &arr[maxi]);
		begin++; end--;
	}
}//时间复杂度尾o(n^2)

堆排序

向上调整算法(重要)

我们先使用画图来解释一下这个调整的方法是什么?假设我们要建立一个大堆,现在堆中已经有了7个元素,下面的就是物理结构

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_05

逻辑结构:

详解希尔排序,直接插入排序,选择排序(c实现)_父节点_06

现在我们要再次插入一个元素是10,如果不做任何的调整逻辑结构就就会变成下面这样:

详解希尔排序,直接插入排序,选择排序(c实现)_父节点_07

这很明显不符合大堆的特点所以这里我们就要从10开始,往上先找到10的父节点也就是2,比较谁大,子节点大则交换让2和10换位,继续往上判断直到那一次父节点大于了字节点或是到达堆顶时停止。

经过这样的调整之后逻辑结构也就会变成下图:

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_08

概括的说这个函数也就是通过子节点找到父节点(i-1)/2也就是父节点的下标(i为子节点下标)。然后比较父节点和子节点的大小判断是否需要交换,(这里分情况)大堆:子节点大于父节点则要交换,直到父节点大于子节点或是直到没有父节点(父节点的下标越界了)存在了。小堆:子节点小于父节点则要进行交换,直到父节点小于子节点,或是没有父节点(父节点的下标越界了)存在了。

下面我们就来实现这个算法

void AdjustUp(HPDataType* a, int child)//通过父节点找到孩子节点
{
	int parent = (child - 1) / 2;//这个是父节点和子节点的关系()
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;//重新为父节点赋值,确保循环继续
		}
		else
		{
			break;//如果父节点大于此时插入的新节点不用继续调整,直接break即可,因为我这里做的是大堆,所以如果这里父节点大于了
			//子节点
		}
	}
}

还要注意的一点是对于向上调整算法,我们必须保证所要修改的节点的上面已经是一个大堆或是小堆了,否则这种算法是会出错的。

向下调整算法(重要)

首先我们知道向上调整算法是通过孩子节点找到父节点,然后比较再进行交换,而向下调整算法则是我们传过去的是父节点的下标,通过父节点找到子节点,再比较父节点和子节点是否需要交换,和向上调整算法一样,向下调整算法也需要保证所调节节点下方的数据已经是一个堆了。

下面我来图解一下向下调整算法

就拿下面的例子来解释:

这是物理结构

详解希尔排序,直接插入排序,选择排序(c实现)_父节点_09

这是逻辑结构:

详解希尔排序,直接插入排序,选择排序(c实现)_数组_10

我们让10和1的位置互换然后删除10的位置,我们的逻辑结构也就变成了下面这样:

详解希尔排序,直接插入排序,选择排序(c实现)_父节点_11

需要注意通过父节点找到字节点的过程中,如果你是大堆那么你要找的肯定是两个子节点中大的那个进行比较,如果是小堆肯定是要和小的那个进行比较。那么这里我在这里不推荐使用if else来写那样写会造成逻辑的重复。我这里推荐使用假设法,我们先假设左节点是目标节点将其赋给一个变量,我们再拿这个变量和右节点比较如果右节点更符合条件则修改变量值,这样我们也能找到目标节点。

继续上面的这个图,将1和8比较很明显8更大则交换1和8。再比较1和4很明显4更大再次交换,当1来到最顶层之后没有孩子节点了算法结束。从这里我们也能知道和向上调整算法一样向下调整算法的结束条件也有两个一个是,交换中途满足了条件算法结束,另外一种也就是调整到了没有字节点的位置算法依旧也是结束。

经过向下调整之后上面的逻辑结构就会变成下面这样:

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_12

下面我们来完成代码:

void AdjustDown(HPDataType* a,int n, int parent)//这里的n就是元素个数
{
	int child = 2 * parent + 1;//我这里假设child就是最大的数
	while (child < n)//因为我们是通过父节点去寻找子节点的,所以当求得到的子节点大于元素个数时,代表parent来到了
		//最底部的节点不能再交换,否则会越界访问
	{
		if (child+1<n&&a[child] < a[child + 1])//如果右孩子大于左孩子就改变child,如果没有右孩子则不需要判断,
			//这里要需要考虑如果改父节点如果只有1个子节点的情况。
      //如果你要建立小堆只用改变为小于号即可
		{
			child++;
		}
		if (a[parent] < a[child])
    //建立小堆这里也要改变为小于号
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;//如果父亲大于最大的孩子则满足大堆条件
		}
	}
}

推排序的实现方法一

现在给你一个数组我们怎样将它排序成有序呢?这里假设我们是要将数组排序成降序的。那么我首先就会将数组里面的所有数据都建立成一个大堆,然后先获取堆顶地元素将其放入原数组中,再删除堆顶的元素。不断地删除知道堆中没有数据也就完成了数组地排序。在写代码之前我们先来考虑一下这种写法的缺点。

缺点

  • 我们要使用这种方法那么你首先就要完成一个能够增删查改的堆也就是至少要完成堆函数的初始化,插入,删除,获取堆顶函数。
  • 这种方法的空间利用率很低,而且需要我们拷贝数据

我们待会使用的方法二就能很好的解决这两个问题,下面我先将这个代码完成。

// 对数组进行堆排序
void HeapSort(int* a, int n)
{
	Heap hp;
	HeapCreate(&hp);
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp,a[i]);//将数组里面的数字全部储存进堆里面
	}
	int i = 0;
	while (!HeapEmpty(&hp))
	{
		int val = HeapTop(&hp);
		a[i++] = val;
		HeapPop(&hp);
	}
	HeapDestory(&hp);//使用这种堆排序的话你建立大堆最后出现的就是降序,而小堆就是升序
	//这种方法可行但是需要我们1先写一个完整的堆,并且这样的写法空间复杂度很高,而且需要我们拷贝数据
}

堆排序的实现方法二

那么我们能否就将原数组先建立成一个堆呢?这肯定是可以的,在我们将原数组建立成一个堆之后(假设是大堆),我们又要如何完成排序呢?我们可以在完成堆之后使用和删除函数差不多的思路,将堆顶的最大元素和堆底的元素交换,之后我们在使用向下调整算法的时候,不将最后的元素包含起来,再将次大的元素找出来,放到堆顶后交换放到原数组倒数第二的位置,再次向下排序,这样我们也就能知道建立大堆最后得到的就是一个升序的数组。

下面我通过画图来解释上面的这个过程。

下面的这个就是无序的一个数组。

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_13

我们将其建立成一个大堆之后的物理结构和逻辑结构如下图:


详解希尔排序,直接插入排序,选择排序(c实现)_子节点_14

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_15

现在我们交换9和4然后再从堆顶开始重新向下调整(不包括9的位置),然后我们再来看新的逻辑和物理结构:

详解希尔排序,直接插入排序,选择排序(c实现)_数组_16


再然后我们继续将8和1交换之后再向下调整的时候将8和9的位置都不包含的情况下再次建立大堆,这样直到最后我们就能够得到一个升序的有序数组。

下面我们来重点讲解堆排序中如何建立一个大堆/小堆

堆排序中建立堆的方法

首先在堆排序中要建立一个堆就要使用向下调整算法和向上调整算法,由此建立堆也就有了两种方法一种是使用向上调整的算法建立堆,一种是使用向下调整的方法建立堆。这两种算法的时间复杂度的计算我会在下一篇博客中指出。

向上调整建堆

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_13

要记住使用向上调整算法,传过去的是孩子节点,算出来的是父节点。

我在向上调整算法的那一节说过,我们在对一个节点使用向上调整算法的时候,我们必须保证在这个节点上面的节点已经是一个大堆/小堆了。如果我们从最后的节点开始向上调整的话肯定是不行的,很明显在最后的节点前面的节点不是一个堆。所以我们这里要从第二个数据开始向上调整,因为单个数据默认就是一个堆,那么在调整完两个数字后,我们在从第三个数子开始往上调整,直到我们将所有的数字调整完成,最后我们就能够得到一个大堆或是小堆。

下面是代码:

for (int i = 1; i < n; i++)
	{
		AdjustUp(a,i);
	}//通过向上调整我们就能将数组a调整为一个大堆
向下调整建堆

详解希尔排序,直接插入排序,选择排序(c实现)_子节点_13

向下调整算法,传过去的是父节点,得出的是子节点

和向上调整算法一样,在对一个节点使用向下调整算法的时候,必须保证这个节点下方的节点们必须是大堆或是小堆。要知道单个节点默认是堆所以我们要找到第一个非叶子节点,即最后一个父节点,然后调整这个父节点下方的子节点,然后减去1找到倒数第二个父节点不断的向上,直到最后调整根节点。就完成了建立堆

下面是代码:

for (int i = (n - 1 - 1) / 2; i >= 0; i--)//n-1代表的是最后一个元素的下标再次-1除以2
  //是找到这个元素的父节点,也就是最后一个父节点
	{
		AdjustDown(a, n, i);
	}

下面我们来完善推排序的代码:

void HeapSort(int* a, int n)
{
	// 对数组进行堆排序
//要使用堆排序我们首先就要建立堆,建立堆有两种方法,其中一种是向上建立堆,一种是向下建立堆,同时排序也有两种排序方式,
//其中一种是升序排列,一种是降序排列
//这里我们需要知道如果你要升序排列那么你要建立大堆
//而如果你要降序排列那么你就要创建小堆
  //至于这是为什么,堆排序的思路也就是我们让堆顶的元素和堆底的元素交换(如果这是大堆那么堆顶的元素就是最大的元素),然后将最后一个元素删除,让上面的元素全部重新调整
  //再次取出次大的元素放到倒数第二个元素,那么到最后我们就会得到一个升序的数列
	//首先是创建一个堆有两种建立方式一种是向上建立堆的方法,一种是向下建立堆的方式这里和上一种方法最不同的一点就是我们直接将待排序的数组建立成堆
	//然后有两种方法建立堆
	//向上调整建堆和向下调整建堆
	//首先就是向上调整建立堆,这里建立堆使用的是原数组即我们不再重新创建一个堆而是将数组建立成堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a,i);
	}//通过向上调整我们就能将数组a调整为一个大堆
	//因为做成的是大堆所以最后排序的时候出来的是一个升序
	//现在开始使用向下调整建立大堆
	//向上调整要满足的条件就是左右子树必须是一个大堆
	//所以我们从堆的最顶层开始向下调整
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//n-1代表的是最后一个元素的下标再次-1除以2是找到这个元素的父节点
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);//交换头尾两个元素
		AdjustDown(a, end, 0);//向下调整重新建立堆,这里的end指的是元素个数,因为我们删除了第n-1个元素,但是第n-1个元素
		//之前的元素个数本来就是n-1
		end--;//最后让end--将最后一个元素删除让其不再进入调整
	}


}

这里我详细解释一下这一段

while (end > 0)
	{
		swap(&a[0], &a[end]);//交换头尾两个元素,这里我是写了一个简单的交换两个数的函数
		AdjustDown(a, end, 0);//向下调整重新建立堆,这里的end指的是元素个数,因为我们删除了第n-1个元素,但是第n-1个元素
		//之前的元素个数本来就是n-1
		end--;//最后让end--将最后一个元素删除让其不再进入调整
	}

我们要知道在AdjustDown(a, end, 0);这里面的end代表的是要向下调整的数目,而在while里面end代表的是下标,这里我们要知道最后一个元素的下标等于前面元素的个数,例如一个数组 1 2 3 4。4的下标是3,而3恰好就是前面元素的个数。那么这里也是一样的,我们现在需要调整的是最后一个元素前面的所有元素,所以我们这里将end传过去代表的是要调整的元素个数。最后我们让end--等于交换倒数第二个元素,再调整倒数第二个元素前面的所有元素。

希望这篇博客能对你有所帮助,如果有错误请指出。