【1++的数据结构初阶】之八大排序

作者主页:进击的1++
专栏链接:1++的数据结构初阶

文章目录

  • 一,前言
    • 1.1 为什们要学排序?
    • 1.2 如何学好排序?
  • 二,排序
    • 2.1 冒泡排序
      • 2.1.1 冒泡排序的原理
      • 2.1.2 冒泡排序的实现
      • 2.1.3 冒泡排序的稳定性及其复杂度分析
    • 2.2 插入排序
      • 2.2.1 插入排序的原理
      • 2.2.2 插入排序的实现
      • 2.2.3插入排序的稳定性及其复杂度分析
    • 2.3 希尔排序
      • 2.3.1 希尔排序的原理
      • 2.3.2 希尔排序的实现
      • 2.3.3 希尔排序的稳定性及其复杂度分析
    • 2.4 选择排序
      • 2.4.1 选择排序的原理
      • 2.4.2 选择排序的实现
      • 2.4.3 选择排序的稳定性及其复杂度分析
    • 2.5 堆排序
      • 2.5.1 堆排序的原理
      • 2.5.2 堆排序的实现
      • 2.5.3 堆排序的稳定性及其复杂度分析
    • 2.6 快速排序
      • 2.6.1 快速排序的原理(递归)
      • 2.6.2 快速排序的实现(递归)
      • 2.6.3 快速排序key的选择问题及方法
      • 2.6.4 快速排序之挖坑法(递归)
      • 2.6.5 快速排序之前后指针法(递归)
      • 2.6.6 快速排序(非递归)
        • 2.6.6.1 快排非递归的原理
        • 2.6.6.2 快排非递归的实现
      • 2.6.7 快速排序的稳定性及其复杂度分析
    • 2.7 归并排序
      • 2.7.1 归并排序的原理
      • 2.7.2 归并的实现(递归)
      • 2.7.3 归并的非递归原理
      • 2.7.4 归并的实现
      • 2.7.5 归并的稳定性及其复杂度
    • 2.8 计数排序
      • 2.8.1 计数排序的原理
      • 2.8.2 计数排序的实现
      • 2.8.3计数排序的稳定性及其复杂度分析
  • 三. 总结

一,前言

1.1 为什们要学排序?

排序是一种重要的算法,它可以将数据按升序或者逆序进行排列,排序的主要目的是将数据按一定的顺序组织起来,以便于后续的处理或操作。在实际应用中,排序可以提高程序的效率和准确性,减少计算机和储存的时间开销。

1.2 如何学好排序?

学好排序,首先要理解排序的基本概念和原理;能够实现各种排序;能够理解并计算出各种排序算法的时间复杂度,空间复杂度,以及稳定性;了解排序算法的应用场景及其优缺点。

注:本文所有排序顺序都为升序。

二,排序

2.1 冒泡排序

2.1.1 冒泡排序的原理

单趟原理:从0下标位置开始,当前元素与前一个元素比较,若当前元素大于前一个元素,则两元素交换位置,直到所有元素比较完,此时数组中最大的元素将会在最后一个位置。

多趟原理:控制最后一个元素的位置,依次减小,每趟在未确定位置的元素中选出最大的元素,进行n-1趟后,数组就排好了。

原理图如下:

【1++的数据结构初阶】之八大排序_第1张图片

2.1.2 冒泡排序的实现

void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n-1; j++)
	{
		for (int i = 1; i < n-j; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
			}
		}
	}
}

冒泡的优化版:

void BubbleSort(int* a, int n)
{
	int flag = 0;
	for (int j = 0; j < n - 1; j++)
	{
		for (int i = 1; i < n - j; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				flag = -1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

2.1.3 冒泡排序的稳定性及其复杂度分析

稳定性是指: 在排序过程中,前后两个相等的元素相对位置不变则算法稳定。
在冒泡排序中,只有后一个元素大于前一个元素是,才交换位置,相等元素的相对位置不会发生变化,因此冒泡是稳定的 。

时间复杂度分析:冒泡的非优化版中:最好的时间复杂度与最坏的时间复杂度都为一个公差为一的等差数列(Sn=n*a1+n(n-1)d/2)则其时间复杂度为O(N^2) ;
在优化版中,当数组有序时,其能达到最好的时间复杂度,为O(N),最坏的时间复杂度仍为O(N^2)。

空间复杂度分析:冒泡算法不需要额外的空间,因此空间复杂度为O(1)。

2.2 插入排序

2.2.1 插入排序的原理

从第二个数开始,若比前一个数小,则插入到前一个数的前面,否则再往前继续比较,直到前面没有数与其比较。再进行第三个数是插入,直到数组全部插入完毕,排序完成。

原理图如下:

【1++的数据结构初阶】之八大排序_第2张图片

2.2.2 插入排序的实现

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int end = i - 1;
		int tmp = a[i];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

2.2.3插入排序的稳定性及其复杂度分析

稳定性:只有插入元素小于被插元素时,才会进行插入。因此,插入排序时稳定的。

时间复杂度:当数组有序时,有最好的时间复杂度为:O(N)。当数组逆序时,有最坏时间复杂度,为公差为一的等差数列,即为O(N^2)。
空间复杂度:O(1)。

2.3 希尔排序

2.3.1 希尔排序的原理

在插入排序中,我们的步长为1,在希尔排序中我们将初始步长设为n/2,一次插排完成后,将步长除以2,直到步长小于1,排序完成。希尔排序实际就是,进行多趟不同步长的插入排序(要注意的最最后一次的步长必须为1)。

原理图如下:

【1++的数据结构初阶】之八大排序_第3张图片

2.3.2 希尔排序的实现

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 0)
	{
		gap /= 2;
		for (int i = 0; i < n-gap; i++)
		{
			int end = i;
			int tmp = a[i + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}

}

2.3.3 希尔排序的稳定性及其复杂度分析

稳定性:由于希尔排序开始时步长大于一,在插入时,会跳过一些元素,若此时刚好跳过的元素中有与插入到前面的元素相等的元素,则相对位置发生了变化,则希尔排序不稳定。

时间复杂度:最好情况:O(N);最坏情况:O(NlogN)。
空间复杂度:O(1)。

2.4 选择排序

2.4.1 选择排序的原理

定义一个max 和 min ,选出一个最大元素与一个最小元素,确定其位置,接着选出次小与次大的元素…直到所有元素都确定好位置,排序完成。

原理图如下:

【1++的数据结构初阶】之八大排序_第4张图片

2.4.2 选择排序的实现

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n-1;
	while (left < right)
	{
		int maxi = left;
		int mini = right;
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[left], &a[mini]);
		if (left == maxi)
		{
			maxi = mini;
		}
		Swap(&a[right], &a[maxi]);
		left++;
		right--;
	}
}

2.4.3 选择排序的稳定性及其复杂度分析

稳定性:上图我们得出选择排序是不稳定的。

时间复杂度:选择排序在最好最坏情况下,时间复杂度都为O(N^2)。
空间复杂度:为O(1)。

2.5 堆排序

2.5.1 堆排序的原理

堆排序中,升序建大堆,降序建小堆。为什么要这样建这样的堆呢?在升序中,建大堆,将最大的元素放在堆顶,将最后一个元素与其堆顶交换,这样最大值就到了最后的位置,再继续调整建堆,调整队尾的位置及n的大小。直到n为0时,堆排序完成。

原理图如下:
【1++的数据结构初阶】之八大排序_第5张图片

2.5.2 堆排序的实现

void AdjustDown(int* a, int parent, int n)
{
	int child = parent * 2 + 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 = parent * 2 + 1;	
		}
		else
		{
			break;
		}
				
	}
}

void HeapSort(int* a, int n)
{
	//向下调整建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

	//向下调整排序
	int end = n-1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
}

2.5.3 堆排序的稳定性及其复杂度分析

稳定性:不稳定。

时间复杂度:最坏的情况:O(NlogN);最好的情况:O(NlogN)。
空间复杂度:O(1)。

2.6 快速排序

2.6.1 快速排序的原理(递归)

先选出一个key值,记其下标为keyi,左右两指针分别向中间靠拢,左指针找比key大的值,右指针找比key小的值,然后进行交换,直到左右两指针相遇,再把key值与左指针指向的值交换,此时,我们会发现,比key小的值到再key 的左边,比key 大的值都在key 的右边(要注意的是,为了是左右两指针相遇时所共同指向的值小于key,则在开始时,我们先让右指针先走)。
这是一趟的原理,对于多趟:类似于二叉树的形式,将单趟走完后key 的位置分为两个区间,分别进行单趟排序。以此类推,直到最小区间之有一个元素。此算法用递归是比较容易实现的。

原理图如下:
【1++的数据结构初阶】之八大排序_第6张图片
【1++的数据结构初阶】之八大排序_第7张图片

2.6.2 快速排序的实现(递归)

void QuickSort(int* a, int left,int right)
{
	if (right <= left)
	{
		return;
	}
	int keyi = left;
	int begin = left;
	int end = right;
	while(right>left)
	{
		while (a[right]>=a[keyi] && right>left)
		{
			right--;
		}
		while (a[left] <= a[keyi] && right > left)
		{
			left++;
		}
		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi+1, end);


}

2.6.3 快速排序key的选择问题及方法

上述这种写法在有序和逆序的情况下时间复杂度最差,是一个等差数列,复杂度为O(N^2)。
这是由于我们的key值是每次都取最左边造成的,我们从选key值下手,有两种方法:随机选key;三数取中。
代码如下:

//随机选key
	int randi = left + (rand() % (right - left));
	Swap(&a[randi], &a[left]);
	//三数取中
	int midi = Getmidi(a, left, right);
	Swap(&a[midi], &a[left]);
	int keyi = left;

2.6.4 快速排序之挖坑法(递归)

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int holei = left;
	int key = a[left];
	int begin = left;
	int end = right;
	while (right > left)
	{
		while (left<right && a[right] >= key)
		{
			right--;
		}
		a[holei] = a[right];
		holei = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[holei] = a[left];
		holei = left;
	}
	a[holei] = key;
	QuickSort(a, begin, holei - 1);
	QuickSort(a, holei + 1, end);

}

2.6.5 快速排序之前后指针法(递归)

void QuickSort(int* a,int left,int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int prev = left;
	int cur = left+1;
	int keyi = left;
	while (cur<=right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
	
}

2.6.6 快速排序(非递归)

2.6.6.1 快排非递归的原理

我们通过观察可以发现递归所做的事情就是将新由keyi分开的新的区间再进行一次单趟排序。那么,我们是否能够通过将区间的两端保存在某种数据结构中,当用的时候再拿出来呢?栈就是最佳的选择。

原理图如下:
【1++的数据结构初阶】之八大排序_第8张图片
【1++的数据结构初阶】之八大排序_第9张图片

2.6.6.2 快排非递归的实现

void QuickSortNoR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, left);
	STPush(&st, right);
	while (!STEmpty(&st))
	{
		right=STTop(&st);
		STPop(&st);
		left = STTop(&st);
		STPop(&st);
		int begin = left;
		int end = right;
		int keyi = left;
		while (right > left)
		{
			while (a[right]>=a[keyi] && right>left)
			{
				right--;
			}
			while (a[left] <= a[keyi] && right > left)
			{
				left++;
			}
			Swap(&a[right], &a[left]);
		}
		Swap(&a[keyi], &a[left]);
		keyi = left;
		if (keyi + 1 < end)
		{
			STPush(&st, keyi + 1);
			STPush(&st, end);
		}
		if (keyi - 1 > begin)
		{
			STPush(&st, begin);
			STPush(&st, keyi - 1);
		}

	}	
}

2.6.7 快速排序的稳定性及其复杂度分析

稳定性:不稳定

时间复杂度:在不进行keyi的处理时,最坏的时间复杂度:O(N^2)。最好的时间复杂度为O(NlogN)。
空间复杂度:递归为O(NlogN);非递归为O(N)。

2.7 归并排序

2.7.1 归并排序的原理

我们先来将递归的单趟排序,将以数组从中间分为前后两部分,前面部分的元素与后面的元素依次比较,将较小的元素保存在另一个新开辟的大小与当前数组相等的数组中。
归并的递归就是将数组进行不断分割,直到前后界限相同时返回,然后进行上述的单趟排序。

原理图如下:

【1++的数据结构初阶】之八大排序_第10张图片

2.7.2 归并的实现(递归)

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (end <= begin)
	{
		return;
	}
	int mid = (begin + end) / 2;
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	_MergeSort(a, tmp, begin1, end1);
	_MergeSort(a, tmp, begin2, end2);
	//单趟排序
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] > a[begin2])
		{
			tmp[i++] = a[begin2++];
		}
		else
		{
			tmp[i++] = a[begin1++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a+begin, tmp+begin, sizeof(int) * (end - begin + 1));

}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);
	_MergeSort(a, tmp, 0, n - 1);
	free(tmp);
	tmp = NULL;
}

2.7.3 归并的非递归原理

模拟上述递归的方式,从最底层的区间开始,进行单次排序后,将间距乘2,再次进行单趟排序,直到间距大于数组元素数量。要注意的是在数组元素数量非偶数的情况下,要对区间边界进行调整,防止数组越界。

2.7.4 归并的实现

void MergeSortNoR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + gap * 2 - 1;
			int j = i;
			if (begin2 >= n )
			{
				begin2 = n ;
				end2 = n - 1;
			}
			else if (end2 >=n )

			{
				end2 = n - 1;
			}
			else if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			//printf("[%d,%d][%d,%d]  ", begin1, end1, begin2, end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] > a[begin2])
				{
					tmp[j++] = a[begin2++];
				}
				else
				{
					tmp[j++] = a[begin1++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}

		}
		//printf("\n");
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

2.7.5 归并的稳定性及其复杂度

稳定性:稳定
时间复杂度(递归):O(NlogN)。
空间复杂度:O(N)。

2.8 计数排序

2.8.1 计数排序的原理

先找出最大值与最小值,确定要开辟空间的大小,并将开辟的新数组初始化为0,然后遍历数组,将每个元素减去最小值作为下标,并让计数数组加1.遍历完成后,便可以统计出每个元素的个数,最后,再次遍历这个数组,用其下标加最小值一次储存到原来数组中。

2.8.2 计数排序的实现

void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	assert(count);
	memset(count, 0, sizeof(int) * range);
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
	free(count);
}

2.8.3计数排序的稳定性及其复杂度分析

稳定性:不稳定。
时间复杂度:O(N+范围)。
空间复杂度:O(范围)。

三. 总结

排序不论是在面试还是在实际工作的运用中都非常 重要,希望次篇文章能够大家带来帮助,如有错误与建议,可以私信或评论区中指正出来。
当可以将文章从头你看到这里时,那么我们有共同的理想与目标,我祝你学习进步,早日能够拿到心仪的offfer。

你可能感兴趣的:(1++的数据结构,数据结构,排序算法)