详解—数据结构—<常用排序>基本实现和代码分析

目录

一.排序的概念及其运用

1.1排序的概念

1.2排序运用​编辑

1.3 常见的排序算法​编辑

二.常见排序算法的实现

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.2.1. hoare版本

2.3.2.2. 挖坑法

2.3.2.3. 前后指针版本

2.3.2.4. 非递归版本

2.3.3 快速排序优化

2.3.3.1. 三数取中法选key

2.3.3.2. 递归到小的子区间时,可以考虑使用插入排序

2.4 归并排序

2.5 非比较排序

三.排序算法复杂度及稳定性分析​编辑


一.排序的概念及其运用


1.1排序的概念

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

1.2排序运用

1.3 常见的排序算法
详解—数据结构—<常用排序>基本实现和代码分析_第1张图片

912. 排序数组 - 力扣(LeetCode)力扣的排序OJ 题可以用各种排序跑这个OJ

二.常见排序算法的实现

2.1 插入排序

2.1.1基本思想:

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

实际中我们玩扑克牌时,就用了插入排序的思想

详解—数据结构—<常用排序>基本实现和代码分析_第2张图片

2.1.2直接插入排序:

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

void InsertSort(int a[], int n)
{
	//i 每一个数都算是插入进去的
	for (int i = 0;i < n - 1;i++)
	{
		//end 数组内数据个数
		int end = i; 
		//tmp 为插入数据
		int tmp = a[end + 1];

		//插入数据与数组内数据比较,如果插入数据比数组最后一个数据大 好 直接原地插入 
		//跳出循环 ,否则,跟数组内数据比较 找到数组内合适的位置插入
		while (end>=0) // 数组内数据遍历到a【0】
		{
			if (tmp < a[end]) //插入数据 与 数组最后位置比较
			{
				a[end + 1] = a[end];  // 小于 往前走 
				--end;
			}
			else
			{  
				break;    //大于 结束
			}
			a[end + 1] = tmp; //
		}
		
	}
}

直接插入排序的特性总结:

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

2.1.3 希尔排序( 缩小增量排序 )

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

详解—数据结构—<常用排序>基本实现和代码分析_第3张图片

void ShellSort(int* a, int n)
{
	//希尔排序
	 
	//1. 先预排序
	// 预排序 就是  把要排序数组分成 gap组 然后开始比较 交换
	// 
	//2.在插入排序

	// gap > 1 预排序
	// gap == 1 直接插入排序
	int gap = n;
	while (gap > 1) //
	{
		// gap = gap / 2; 
		gap = gap / 3 + 1;// 如果不加1  就有可能到不了 1 

		//当 gap  =1 时就是直插  ,gap 越小 与排序越接近排序好的状态
		for (int i = 0; i < n - gap; ++i)
		{
			int end = i;
			int tmp = a[end + gap]; 

			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}

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

希尔排序的特性总结:

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

2.2 选择排序

2.2.1基本思想:

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

2.2.2 直接选择排序:

在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素


若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换


在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

详解—数据结构—<常用排序>基本实现和代码分析_第4张图片

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;

	while (begin < end)
	{
		int mini = begin, maxi = begin;

		for (int i = begin + 1; i <= end; ++i)
		{
			if (a[i] < a[mini])//遍历数组 找最小数据
			{
				mini = i;
			}

			if (a[i] > a[maxi])//找最大数据
			{
				maxi = i;
			}
		}

		Swap(&a[begin], &a[mini]); //交换最开始和 最小的数据
		if (maxi == begin)    //如果最大的是最开始的数据,上面交换最大的数据被换到了最小的数据位置上
			maxi = mini;      //把最小数据的位置交给最大数据

		Swap(&a[end], &a[maxi]);//交换最后和 最大的数据
		++begin;
		--end;
	}
}

直接选择排序的特性总结:

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

2.2.3 堆排序

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

详解—数据结构—<常用排序>基本实现和代码分析_第5张图片

//向下调整建堆
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		// 确认child指向大的那个孩子
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		// 1、孩子大于父亲,交换,继续向下调整
		// 2、孩子小于父亲,则调整结束
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//堆排
void HeapSort(int* a, int n)
{
	// 向下调整建堆 -- O(N)
	// 升序:建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

堆排序的特性总结:

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

2.3 交换排序

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

2.3.1冒泡排序
详解—数据结构—<常用排序>基本实现和代码分析_第6张图片

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

冒泡排序的特性总结:

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

2.3.2 快速排序

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

将区间按照基准值划分为左右两半部分的常见方式有:

1. hoare版本
2. 挖坑法
3. 前后指针版本

4.非递归版本

2.3.2.1. hoare版本

1、选出一个key,一般是最左边或是最右边的。
2、定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走)。
3、在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int left = begin, right = end;
	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[left], &a[keyi]);
	keyi = left;

	// [begin, keyi-1]  keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
2.3.2.2. 挖坑法

挖坑法思路与hoare版本(左右指针法)思路类似
1.选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑


2、还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)

后面的思路与hoare版本思路类似在此处就不说了

//快速排序法  挖坑法
void QuickSort1(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	int left = begin,right = end;
	int key = arr[begin];
	while (begin < end)
	{
		//找小
		while (arr[end] >= key && begin < end)
		{
			--end;
		}
		//小的放到左边的坑里
		arr[begin] = arr[end];
		//找大
		while (arr[begin] <= key && begin < end)
		{
			++begin;
		}
		//大的放到右边的坑里
		arr[end] = arr[begin];
	}
	arr[begin] = key;
	int keyi = begin;
	//[left,keyi-1]keyi[keyi+1,right]
	QuickSort1(arr, left, keyi - 1);
	QuickSort1(arr, keyi + 1, right);
}
2.3.2.3. 前后指针版本

1、选出一个key,一般是最左边或是最右边的。


2、起始时,prev指针指向序列开头,cur指针指向prev+1。


3、若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur到达end位置,此时将key和++prev指针指向的内容交换即可。

经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。

然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作

//快速排序法  前后指针版本
void QuickSort2(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	int cur = begin, prev = begin - 1;
	int keyi = end;
	while (cur != keyi)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)
		{
			swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	swap(&arr[++prev],&arr[keyi]);
	keyi = prev;
	//[begin,keyi -1]keyi[keyi+1,end]
	QuickSort2(arr, begin, keyi - 1);
	QuickSort2(arr, keyi + 1, end);

}
2.3.2.4. 非递归版本
void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int keyi = left;
		int prev = left, cur = right + 1;
		while (cur <= end)
		{
			// 找到比key小的值时,跟++prev位置交换,小的往前翻,大的往后翻
			if (a[cur] < a[keyi] && ++prev != cur)
				Swap(&a[prev], &a[cur]);

			++cur;
		}

		Swap(&a[prev], &a[keyi]);
		keyi = prev;

		// [left, keyi-1] keyi [keyi+1, right]
		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}

		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
	}

	StackDestroy(&st);
}

2.3.3 快速排序优化

2.3.3.1. 三数取中法选key
int Fetchmid(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else // a[begin] > a[mid]
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
2.3.3.2. 递归到小的子区间时,可以考虑使用插入排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	//小区间优化
	if (end - begin < 12)
	{
		//当小于12 个数的时候用直接插入法插入
		InsertSort(a+begin, end - begin);
	}
	else
	{
		int left = begin, right = end;
		int keyi = Fetchmid(a,begin, end);
		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[left], &a[keyi]);
		keyi = left;

		// [begin, keyi-1]  keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

快速排序的特性总结:

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

3. 空间复杂度:O(logN)
4. 稳定性:不稳定

2.4 归并排序

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

详解—数据结构—<常用排序>基本实现和代码分析_第7张图片

详解—数据结构—<常用排序>基本实现和代码分析_第8张图片

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

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

	// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
	int rangeN = 1;
	while (rangeN < n)
	{
		for (int j = 0;j < n;j += 2 * rangeN)
		{
			int begin1 = j; int end1 = j+ rangeN -1;
			int begin2 = j+ rangeN, end2 = j + 2 * rangeN - 1;

			if (end1 > n)//end1越界  begin2 和 end2 肯定越界
			{
				break;
			}
			else if (begin2 > n)//begin2 越界  end2 肯定越界
			{
				break;
			}
			else if (end2 > n)
			{
				end2 = n - 1;
			}

			int i = j;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[i++] = a[begin1++];
				}
				else
				{
					tmp[i++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[i++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[i++] = a[begin2++];
			}
			// 归并一部分,拷贝一部分
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
		
		}
		rangeN *= 2;
	}

	free(tmp);
	tmp = NULL;
}

归并排序的特性总结:

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

2.5 非比较排序

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
详解—数据结构—<常用排序>基本实现和代码分析_第9张图片

void CountSort(int* arr, int size)
{
	int i;
	int minValue = arr[0];
	int maxValue = arr[0];
	int range = 0;
	int* tmp = 0;
	int count = 0;
	for (i = 0; i < size; i++)//计算数据的分散空间
	{
		if (arr[i] < minValue) {
			minValue = arr[i];
		}
		if (arr[i] > maxValue) {
			maxValue = arr[i];
		}
	}
	range = maxValue - minValue + 1;
	tmp = (int*)malloc(sizeof(arr[0]) * size);//申请辅助空间
	if (tmp == NULL)
		return;
	memset(tmp, 0, sizeof(int) * range);//初始化

	for (i = 0; i < size; i++)//统计每个元素出现的次数
	{
		tmp[arr[i] - minValue]++;
	}

	for (i = 0;i < range;i++)//通过统计tmp[];回收元素
	{
		while (tmp[i]--)
		{
			arr[count++] = i + minValue;
		}
	}
	free(tmp);
}

计数排序的特性总结:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(MAX(N,范围))
3. 空间复杂度:O(范围)
4. 稳定性:稳定

三.排序算法复杂度及稳定性分析
详解—数据结构—<常用排序>基本实现和代码分析_第10张图片

详解—数据结构—<常用排序>基本实现和代码分析_第11张图片

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