数据结构:八大排序详解(插入、希尔、选择、堆、冒泡、快速、归并、计数)

一、插入排序

1.1 思想

插入排序思想较为简单,其实现思想为:

要插入第 i 个元素,在前 i - 1个元素已经有序的情况下,此时将第 i 个元素依次与 i - 1、i - 2......2、1元素依次比较,找到其适合进入的位置即可。

可见下面过程图:

 

下面是插入的流程图:

以上为一趟插入排序,插入一个数的情况。

那么,对整个无序的数组进行插入排序,则就是依次对数组的每个元素都进行插入,找到自己的合适位置。即从数组的第二个元素开始依次插入,直到数组的最后一个元素为止。

1.2 实现

// 插入排序
void InsertSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		int end = i;
		int tmp = a[end];//保存要排序的数
		while (end >= 0)
		{
			if (end - 1 >= 0 && tmp < a[end - 1])
			{
				a[end] = a[end - 1];
			}
			else
				break;

			end--;
		}
		a[end] = tmp;
	}

}

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

空间复杂度:O(1)

稳定性:稳定

二、希尔排序

2.1 思想

希尔排序,简单来说就是多组插入排序。

插入排序,其根本是在步长为1的情况下,对每个数进行插入直到数组的最后一个元素。

而希尔排序,其步长是变化的,但最后会变为1,步长为几,则就有几组,为1,则说明所有元素为1组。确定好步长,则就确定了组数,然后分别对每组进行插入排序,之后再变化步长,再进行插入排序,直到步长为1时进行最后一次插入排序。下面通过画图来了解下分组。

因此,确定好了步长,则就确定好了组数。之后则就分别对每组进行插入排序即可。每组排序完后,变化步长,一般步长的变化都为缩小2倍。直到步长为1即可。步长越小,数据就越有序,这点应该大家比较清楚的。

2.2 实现

// 希尔排序
void ShellSort(int* a, int n)
{
	int gap = 5;//首先,确定一个初始步长
	while (gap >= 1)
	{
		gap = gap / 2;//变化步长,最终步长为1
		int i = 1;
		int j = 0;
		for (j = 0; j < gap; j++)//组数(步长为几,组数就为几)
		{
			for (i = j; i < n; i += gap)//各组的插入排序,也就是将插入排序中的1全变为gap
			{
				int end = i;
				int tmp = a[end];
				while (end >= 0)
				{
					if (end - gap >= 0 && tmp < a[end - gap])
					{
						a[end] = a[end - gap];
					}
					else
						break;

					end -= gap;
				}
				a[end] = tmp;
			}
		}
		
	}
}

时间复杂度:O(n^1.25) 到 O(1.6 * n^1.25)

空间复杂度:O(1)

稳定性:不稳定

三、选择排序

3.1 思想

选择排序思想极为简单,即每次从所有元素中选出最大和最小的元素,分别放在元素的起始和终止位置,直至待排序的元素全部排完。

基本流程图如下所示:

其思想较为简单,就是找到最大值和最小值后,交换即可。

但在遇到下面这两种情况时,交换需要注意的一点为:

注意1:

如果先让 left 与 mini 进行交换,那么,在交换时需要注意一种特殊情况(如下):

遇见这种情况时,交换了 left 和 mini 时,此时,最大值移动到了 mini 下标上,但最大值下标还是在 left 位置上,那么需要变化下 maxi 下标了,即让 maxi = mini即可。

注意2:

如果先让 right 与 maxi 进行交换,那么,在交换时需要注意一种特殊情况(如下):

遇见这种情况时,交换了 right 和 maxi 时,此时,最小值移动到了 maxi 下标上,但最小值下标还是在 right 位置上,那么需要变化下 mini 下标了,即让 mini = maxi即可。

3.2 实现

// 选择排序
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left <= right)
	{
		int i = 0;
		int maxi = left;
		int mini = left;//最大值和最小值的初始值
		for (i = left; i <= right; i++)
		{
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
			if (a[mini] > a[i])
			{
				mini = i;
			}
		}
		Swap(&a[left], &a[mini]);//先交换的 left 和 mini 则为注意1
		if (maxi == left)//注意1
		{
			maxi = mini;
		}
		Swap(&a[right], &a[maxi]);
		left++;
		right--;
	}
}

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

空间复杂度:O(1)

稳定性:不稳定

四、堆排序

4.1 何为堆?

要想了解堆排序的思想,首先要了解何为堆,堆是二叉树的一种,属于完全二叉树。

其分为两类:大堆:父亲节点均大于其孩子节点,其根节点为其全部元素的最大值       小堆:父亲节点均小于其孩子节点,其根节点为其全部元素的最小值

因此其性质为:堆中某个节点的值总是不大于或不小于其父节点的值。

例如:

ok,了解完什么是堆后,我们就可以来进行了解堆排序了。

4.2 思想

堆排序即为,先对原数组元素建堆(升序建大堆,降序建小堆),之后每次交换根节点与最后一个元素的值,此时最后一个元素一定为其整个数组的最大值或最小值,因此最后一个元素就已经调整好了,之后再调整根节点(向下调整算法),在进行根节点与倒数第二位元素交换................直至所有元素均在合适位置上。

其实现主要依靠于堆的建立,这里给出一种方法:即向下调整建堆:

建堆的时候首先找到倒数第一个非叶子节点,从这一节点开始进行调整。直至调整至数组的第一个元素。

如升序,其代码实现:

// 堆排序
void AdjustDwon(int* a, int n, int root)//向下调整每次都会找到最大的(升序)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}

		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void Jiandui(int* a, int n)
{
	int i = 0;
	//向下调整建大堆(升序)
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDwon(a, n, i);
	}
	
}

建完堆后,完成排序操作即只需要根节点与最后一个元素交换,交换完调整................交换,调整...............。其现在思维构架图暂未作出,等我下次出个专题进行说明!!!!!

4.3 实现

// 堆排序
void AdjustDwon(int* a, int n, int root)//向下调整每次都会找到最大的
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}

		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	int i = 0;
	//向下调整建大堆
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDwon(a, n, i);
	}
	//堆排序
	for (i = n - 1; i >= 0; i--)
	{
		Swap(&a[0], &a[i]);//交换
		AdjustDwon(a, i, 0);//向下调整
	}
}

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

空间复杂度:O(1)

稳定性:稳定

五、冒泡排序

5.1 思想

冒泡排序,老生常谈了。其思想也较为简单。

两两进行比较交换,使其按照从大到小或从小到大顺序进行排列。每次排序后,就会使最大或最小的数排在元素的尾部。

下面为其过程图:

             

每进行一次冒泡排序,就会使最大的数(升序)排在了最右边,下一次就可以不去与这最大的数比较了。其比较次数也就减小了1。

5.2 实现

// 冒泡排序
void BubbleSort(int* a, int n)
{
	int i = 0, j = 0;
	for (i = 1; i <= n; i++)
	{
		for (j = 0; j < n - i; j++)//每次排好一个最大的数,其比较次数就减一
		{
			if (a[j] > a[j + 1])//升序
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}

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

空间复杂度:O(1)

稳定性:稳定

六、快速排序

6.1 思想

快速排序其思想为:任取全部元素中的某个元素作为基准值,利用这一基准值将全部元素分为两部分,一部分元素小于其基准值,另一部分元素大于其基准值。这两部分分别位于基准值的左右两边。最后重复此过程,直至所有元素都排在指定位置上。

6.2 实现

6.2.1 递归方式

6.2.1.1 实现一  hoare版本(初始版本)

hoare版本的实现方法为:

以上,为1次排序结果,一直重复此过程,直至所有值都在指定位置上。

在上述过程中,key下标的值一直为最左边的值,对此,可以进行优化方案对key值进行调整。

优化方案为:数组最左边、最右边和数组中间这三者值取其中间大小的值。即三数取中。

代码:

// 快速排序递归实现
// 快速排序hoare版本
//三数取中
int midthree(int* a,int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[right])
	{
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return right;
		}
		else
			return mid;
	}
	else
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		else if (a[right] < a[mid])
		{
			return right;
		}
		else
			return mid;
	}

}

int PartSort1(int* a, int left, int right)
{
	int mid = midthree(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}	
	Swap(&a[right], &a[keyi]);

	return right;
}

6.2.2 实现二  挖坑法

挖坑法的实现方法为:

其基本过程如上图所示。

代码实现:

// 快速排序挖坑法
//三数取中
int midthree(int* a,int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[right])
	{
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return right;
		}
		else
			return mid;
	}
	else
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		else if (a[right] < a[mid])
		{
			return right;
		}
		else
			return mid;
	}
}

int PartSort2(int* a, int left, int right)
{
	int mid = midthree(a, left, right);
	Swap(&a[left], &a[mid]);
	int pit = a[left];
	int piti = left;

	while (left < right)
	{
		while (left < right && a[right] >= pit)//右边找小的
		{
			right--;
		}
		a[piti] = a[right];
		piti = right;//新的坑位

		while (left < right && a[left] <= pit)//左边找大的
		{
			left++;
		}
		a[piti] = a[left];
		piti = left;//新的坑位
	}
	a[left] = pit;//结束后,将key值填入坑位
	return left;
}

6.2.3 实现三  前后指针法

前后指针法的实现方法为:

基本思想过程如上图所示:

代码实现:

// 快速排序前后指针法
//三数取中
int midthree(int* a,int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[right])
	{
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return right;
		}
		else
			return mid;
	}
	else
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		else if (a[right] < a[mid])
		{
			return right;
		}
		else
			return mid;
	}
}

int PartSort3(int* a, int left, int right)
{
	int mid = midthree(a, left, right);
	Swap(&a[left], &a[mid]);
	int keyi = left;
	int pre = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])//小于key下标值时
		{
			pre++;//先加加prev
			Swap(&a[cur], &a[pre]);//再交换cur和prev
		}
		cur++;//再加加cur
	}
	Swap(&a[keyi], &a[pre]);//循环结束后,让此时的prev与key进行交换
	return pre;

}

在上述三种方法实现后,该进行递归整体结构的实现了。观察上述三种结构的思想不难发现,其最后结果都使 key 值(6)排在了适合的位置上。

其递归思想如下图:

代码实现(使key排在合适的位置中使用上述三种方式的任意一种都可以):

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

	int pre = PartSort1(a, left, right);//以pre为分界线,分割成左区间:[left,pre - 1] 右区间:[pre+1,right]
	
	QuickSort(a, left, pre - 1);//递归左区间
	QuickSort(a, pre + 1, right);//递归右区间
}

6.2.2  非递归方式

非递归的实现主要是利用 栈 来进行实现,将每次分割形成的两个区间作为栈内元素存入栈内。释放时也是从栈的一端释放。

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack sk;
	StackInit(&sk);
	StackPush(&sk, left);//将区间左端点入栈
	StackPush(&sk, right);//将区间右端点入栈
	while (!StackEmpty(&sk))
	{
		int end = StackTop(&sk);//取出区间右端点
		StackPop(&sk);
		int start = StackTop(&sk);//取出区间左端点
		StackPop(&sk);
		int pre = PartSort1(a, start, end);//进行分割
		//左区间:[start pre - 1] 右区间:[pre + 1 end]
		if (pre < end)
		{
			StackPush(&sk, pre + 1);//区间左端点入栈
			StackPush(&sk, end);//区间右端点入栈
		}
		if (pre > start)
		{
			StackPush(&sk, start);//区间左端点入栈
			StackPush(&sk, pre - 1);//区间右端点入栈
		}
	}

}

七、归并排序

7.1 思想

归并排序的思想为将已经有序的序列进行合并,得到新有序序列,重复这一过程,直至其所有数据有序。(文字表达的不太清楚,可看流程图)

其流程图为:

上述流程图为可以进行等分的情况,但如果数组个数不可以等分时,对于非递归实现时需要对右边区间做一些调整,具体可看代码。

7.2 实现

对于其实现方式来说,有两种方式。一是递归实现(与二叉树的遍历过程大致相似),二是非递归实现。

首先来看递归实现(递归实现较为简洁,就不做说明了)

void MerSort(int* a,int begin,int end,int* tmp)
{
	if (end <= begin)
		return;

	int mid = (end + begin) / 2;//取中,分割成两个区间 [begin  mid] [mid+1  end]

	MerSort(a, begin, mid, tmp);//递归左区间
	MerSort(a, mid + 1, end, tmp);//递归右区间

	//两个无序数组[begin  mid] [mid+1  end]合并为有序数组 tmp 内 ----- 基本方法
	int end1 = mid, end2 = end;
	int begin1 = begin,begin2 = mid + 1;
	int index = begin;
	while (end1 >= begin1 && end2 >= begin2)
	{
		if (a[begin1] > a[begin2])
		{
			tmp[index++] = a[begin2++];
		}
		else
		{
			tmp[index++] = a[begin1++];
		}
	}

	while (end1 >= begin1)
	{
		tmp[index++] = a[begin1++];
	}
	while (end2 >= begin2)
	{
		tmp[index++] = a[begin2++];
	}

	//将有序数组tmp 再拷贝回原数组 a
	for (index = begin; index <= end; index++)
	{
		a[index] = tmp[index];
	}
}

// 归并排序递归实现
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
	}

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

	free(tmp);	
}

接下来为非递归实现,注意:非递归实现时,如果数据个数无法等分则需要对区间进行调整。

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
	}

	int gap = 1;
	while (gap < n)
	{
		int i = 0;

		for (i = 0; i < n; i = i + gap * 2)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			int index = i;

			//2个区间 [begin1  end1][begin2   end2]
			//控制好边界条件,如果end1已经超过了n,那么就不需要再进行其他操作,因为原先区间已经是有序的。
			//同理,begin2超过了n也是如此,直接退出循环即可。又因为,begin2 > end1,因此,当end1大于n的时候,begin2一定大于n。所以他俩可以归为一种情况,那就是begin2 >= n
			//而当end2大于n,begin2未大于n时,任然需要进行归并,但这时必须修正下end2,不修正就越界了,因此,就需要让end2等于数组的最大取值即可。

			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

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

			int j = 0;
			//初始条件 j不能等于begin1,因为在前面的过程中,begin1已经加加了
			for (j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}
			//memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
		}
	
		gap *= 2;
	}

	free(tmp);
}

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

空间复杂度:O(N)

稳定性:稳定

八、计数排序

8.1 思想

计数排序,其思想与名字很相似。

思想:开辟一个新数组,新数组的范围为要排序数组的最大值和最小值之差。新数组的下标表示要排序数组的每个数的数值,每个下标所对应的值表示每个数出现的次数,没有在要排序数组中出现的下标其值为0。之后再将新数组覆盖到原数组即可。

过程图:

其实现有点技巧。

另外,计数排序其应用的场景比较有限,适合在于数据较为集中时使用。其效率较高。

8.2 实现

// 计数排序
void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	int i = 0;
	for (i = 0; i < n; i++)
	{
		if (min > a[i])
			min = a[i];
		if (max < a[i])
			max = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	memset(count, 0, sizeof(int) * 100);//初始化拷贝数组为0
	//统计数据个数
	for (i = 0; i < n; i++)
	{
		count[a[i] - min]++;//技巧1
	}
	//覆盖原数组
	int j = 0;
	for (i = 0; i < range; i++)
	{
		while (count[i]--)//技巧2
		{
			a[j++] = i + min;
		}
	}
}

初始化时用到了内存函数,详情了解可见下面这篇文章详细说明。

写文章-CSDN创作中心

时间复杂度:O(max(N,范围)) 

空间复杂度:O(范围)

稳定性:稳定

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值