您现在的位置是:首页 >技术交流 >排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)网站首页技术交流

排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)

羊羊羊i 2024-10-16 00:01:03
简介排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)

在这里插入图片描述

1. 排序概要

排序: 就是将一串随机数据,按照从小到大、或者从大到小重新排列一遍,使它变成有序的数据,便于人们观察和提取数据。

常见的排序算法有:插入排序、选择排序、交换排序、归并排序。

2. 插入排序

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

直接插入排序

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

//插入排序
void InsertSort(int* a, int n)
{
	//从头往后开始遍历
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int key = a[end + 1];  //保存最后的值
		while(end>=0)
		{

			//比前边小就交换
			if (key < a[end])
			{
				//前面的给后面
				a[end + 1] = a[end];
			}
			else
			{
				break;
			}
			end--;
		}
		a[end + 1] = key;
	}

}

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

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

先选取一个步长gap,把每隔gap长度的数据分成一个个组,所有相距为gap的数据在同一个组,并且对每组的数据进行插入排序,然后缩小gap,重复上述过程,直至gap为1,当gap为1时,所有记录在统一组内排好序。

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	//gap>1,预排
	//gap ==1 ,插入排序
	while (gap > 1)
	{
		gap = (gap / 3) + 1;
		for (int i = 0; i < n - gap; i++)
		{
			//单次排序调整
			int end = i;
			int key = a[end + gap];
			while (end >= 0)
			{
				if (key < a[end])
				{
					a[end + gap] = a[end];
				}
				else
				{
					break;
				}
				end -= gap;
			}
			a[end + gap] = key;
		}
	}
}

特性总结:
1.希尔是对直接插入排序的优化。
2.当gap>1时是预排序,目的是让数组更接近有序。当gap==1时,数组已经接近有序,这样就会很快。预排序可以让大数更快的走向末尾,小数更快的走向开始。
3.时间复杂度:接近于O(N^1.3)
4.空间复杂度:O(1)
5.稳定性:不稳定

3.选择排序

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

直接选择排序

在元素集合0~i-1中找到最小的元素,记住数组元素的下标,若遍历完后,该数组下标的位置不是当前数组的起始位置,则该元素与起始位置交换,重复上述步骤,直到集合剩余一个元素。

//选择排序
void SelectSort(int* a, int n)
{
	//每次选择最大和最小的
	int begin = 0;
	int end = n - 1;

	while (begin < end)
	{
		int min_index = begin;
		int max_index = begin;
		for (int i = begin; i <= end; i++)
		{
			//发现更小的
			if (a[min_index] > a[i])
			{
				min_index = i;
			}
			//发现更大的
			else if (a[max_index] < a[i])
			{
				max_index = i;
			}
		}
		Swap(&a[begin], &a[min_index]);
		//防止最大的数字被换走
		if (max_index == begin)
		{
			max_index = min_index;//把下标再赋回来
		}
		Swap(&a[end], &a[max_index]);
		end--;
		begin++;
	}

}


特性总结:
1.虽然思想很好理解,但是效率低,很少使用。
3.时间复杂度:O(N^2)
4.空间复杂度:O(1)
5.稳定性:不稳定

堆排序

堆排序是利用一种堆的数据结构来进行排序的算法,大堆排升序,小堆排降序,每次只选择堆顶的元素,作为最大值或最小值,直至堆的集合个数为1。

//堆排序(大堆)
//先建堆,从后往前建堆效率更高.
//向下建堆效率高,从后往前,每个结点的左右子树都建成堆
void AdjustDown(int* a, int n, int parent)
{
	//比较左右孩子较小的一个
	//如果条件满足再和父亲交换
	//再看调整的孩子需不需要
	int child = 2 * parent + 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 = 2*parent+1;
		}
		else
		{
			break;
		}
	}

}



//堆排序
void HeapSort(int* a, int n)
{
	//1.先建堆
	//2.然后交换首尾元素
	//再向下调整
	int parent = (n-1-1) / 2;
	while (parent>=0)
	{
		AdjustDown(a, n, parent);
		--parent;
	}
	int end = n-1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);//end是下标
		AdjustDown(a, end, 0); //end 代表个数
		--end;
	}
	
}

特性总结:
1.堆排序使用堆来选数字,效率提升很大
3.时间复杂度:O(NlogN)
4.空间复杂度:O(1)循环建堆
5.稳定性:不稳定

4. 交换排序

交换,根据序列中两个记录的键值的比较来对换这两个记录在序列中的位置。交换排序的特点是:将键值较大的数字向尾部移动,将键值较小的数字向序列的前部移动。

冒泡排序


//冒泡排序
void BubbleSort(int* a, int n)
{
	//从小冒到大
	//重复
	int flag = 0;
	for (int j = 0; j < n - 1; j++)
	{
		//每次排序完一个,将flag置0
		flag = 0;
		for (int i = 0; i < n - 1 - j; i++)
		{
			if (a[i] > a[i + 1])
			{
				flag = 1;
				Swap(&a[i], &a[i + 1]);
			}
		}
		if (flag == 0)
		{
			break;
		}
	}

}

特性总结:
1.时间复杂度:O(N^2)
2.空间复杂度:O(1)
3.稳定性:稳定

快速排序

递归思想:按照先序递归的方法,先找基准值,然后保证左子序列全部小于基准值,右子序列的值全部大于基准值。然后再次划分,直至子序列为1即可。

快速排序框架:

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 if(right - left <= 1)
 return;
 
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 int div = partion(array, left, right);
 
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 QuickSort(array, left, div);
 
 // 递归排[div+1, right)
 QuickSort(array, div+1, right);
}

霍尔版本(hoare)

左边作key,右边先走,找小,遇到小于key的数停下,然后左边走找大,遇到大于key的数停下,交换左右两个位置的数字,left==right。


问什么左边作key,右边先走呢?
要保证小于的位置比key小或者就是key的位置:1.R先走,R停下来,L去遇到R(因为R找的是小,R停下来的位置一定小于key);2.R先走,R没有找到比key小的值,R去遇到了L(相遇的位置是上一轮停下来的位置,要么就是key的位置,要么比key要小)

//快速排序(霍尔法)
void QuickSort_old_hoare(int* a, int begin, int end)
{
	//左边作key,右边先走找小,找到了停下
	//左边再走找大,找到停下,交换
	//重复

	if (begin >= end)
	{
		return;
	}

	//一次排序
	int left = begin;
	int right = end;
	int keyi = begin;
	while (left < right)
	{
		//找小
		while (left < right && a[keyi] <= a[right])
		{
			--right;
		}
		//找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	keyi = left;

	//递归
	QuickSort_old_hoare(a, begin, keyi - 1);
	QuickSort_old_hoare(a, keyi+1, end);


}

函数分开版本:

void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end-begin>10)
	{
		int keyi = Partition2(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi+1, end);
	}
	else
	{
		InsertSort(a + begin, end - begin + 1);
	}
}
//快排的霍尔版本
int Partition1(int* a, int begin, int end)
{
	int left = begin;
	int right = end;
	int keyi = begin;
	
	while (left < right)
	{
		//找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	return keyi;

}

挖坑法

人们所熟知的快排就是这个思想,比霍尔法更容易清晰理解。先将第一个数放在临时变量key中,此时形成一个坑位,然后右边先走找小,遇到小的停下来,将该值赋给坑位,并形成一个新的坑位。并且左边找大,赋值,形成新的坑位,直至两边相遇,将key值赋给左边。


//快速排序(挖坑法)
void QuickSort_dig(int* a, int begin, int end)
{
	//左边作key,右边先走找小,找到了停下
	//左边再走找大,找到停下,交换
	//重复

	if (begin >= end)
	{
		return;
	}

	//一次排序
	int left = begin;
	int right = end;
	int keyi = GetMidIndex(a,left,right);
	int key = a[keyi];
	while (left < right)
	{
		//找小
		while (left < right && a[keyi] <= a[right])
		{
			--right;
		}
		a[keyi] = a[right];
		keyi = right;  //right变为坑
		//找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		a[keyi] = a[left];
		keyi = left;
	}
	a[keyi] = key;

	//递归
	QuickSort_old_hoare(a, begin, keyi - 1);
	QuickSort_old_hoare(a, keyi + 1, end);


}

函数分开版本:


void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end-begin>10)
	{
		int keyi = Partition2(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi+1, end);
	}
	else
	{
		InsertSort(a + begin, end - begin + 1);
	}
}


//挖坑版本
int Partition2(int* a, int begin, int end)
{
	int left = begin;
	int right = end;
	//int keyi = begin;//坑在起始位置
	int keyi = GetMidIndex(a,begin,end);//坑在起始位置
	int key = a[keyi];
	while (left < right)
	{
		//找小
		while (left < right &&a[right] >= key)
		{
			--right;
		}
		a[keyi] = a[right];//填坑
		keyi = right;//换坑
		//找大
		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[keyi] = a[left];//填坑
		keyi = left;//换坑
	}
	a[keyi] = key;
	return keyi;
}

双指针版本

这个版本利用双‘指针’,优化了代码结构,使代码变得更加简洁,但是代码的可读性变差。快指针的作用是找小,慢指针的作用是保留边界,两个指针中间是大数,以翻跟头的形式往前走。

//双指针版本
int Partition3(int* a, int begin, int end)
{
	//prev在起始位置,cur在下一个位置
	//判断cur,若cur小于prev,则prev+1交换
	//大于prev,cur++
	int prev = begin;
	int cur = prev + 1;
	int key = a[begin];

	while (cur <= end)
	{
		当prev+1 == cur时,如果再交换的话就浪费了
		//if (a[cur] < key )
		//{
		//	++prev;
		//	Swap(&a[cur], &a[prev]);
		//}
		//++cur;

		//当prev+1 == cur时,如果再交换的话就浪费了
		if (a[cur] < key&&++prev!=cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		++cur;
	}
	Swap(&a[begin], &a[prev]);
	return prev;
}


void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end-begin>10)
	{
		int keyi = Partition2(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi+1, end);
	}
	else
	{
		InsertSort(a + begin, end - begin + 1);
	}
}

快排优化

为了防止最坏情况下爆栈,我们需要使用三数取中法来优化选择keyi的位置。这样递归的深度接近logN。

//为了防止最坏情况爆栈,我们可以用三数取中法来优化,keyi
// 三数取中法
int GetMidIndex(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[end] > a[begin])
			{
				return begin;
			}
			else
			{
				return end;
			}
		}
	}
	else
	{
		if (a[end] < a[begin])
		{
			return begin;
		}
		else
		{
			if (a[end] > a[mid])
			{
				return mid;
			}
			else
			{
				return end;
			}
		}
	}
}

快速排序非递归

因为快排的思想接近于二叉树的先序遍历,因此我们可以考虑使用栈来模拟递归的过程。核心思想就是:利用栈来保存每次快排的区间。有点类似于层序遍历的方法。


//快排非递归(需要使用栈)
// 要求掌握,递归改非递归
// 递归大问题,极端场景下面,如果深度太深,会出现栈溢出
// 1、直接改循环 -- 比如斐波那契数列、归并排序
// 2、用数据结构栈模拟递归过程
void QuickSort_nonrecursive(int* a, int begin, int end)
{
	//先将左右入栈
	//出栈,调用快排函数
	//调用完了再入栈
	Stack q;
	StackInit(&q);
	StackPush(&q, end);
	StackPush(&q, begin);

	//栈不空
	while (!StackEmpty(&q))
	{
		int left = StackTop(&q);
		StackPop(&q);
		int right = StackTop(&q);
		StackPop(&q);
		如果左小于右说明可以划分,就继续入栈
		//if (left < right)
		//{
		//	int keyi = Partition2(a, left, right);
		//	StackPush(&q, keyi - 1);
		//	StackPush(&q, left);
		//	StackPush(&q, right);
		//	StackPush(&q, keyi + 1);
		//}
		

		int keyi = Partition2(a, left, right);
		//小区间能继续划分,就入栈
		if (left < keyi - 1)
		{
			StackPush(&q, keyi - 1);
			StackPush(&q, left);
		}
		if (keyi + 1 < right)
		{
			StackPush(&q, right);
			StackPush(&q, keyi + 1);
		}


	}
	StackDestroy(&q);
}

特性总结:
1.快排,顾名思义综合性能最好,适应场景最多
2.时间复杂度:O(NlogN)
3.空间复杂度:O(logN)
4.稳定性:不稳定

5. 归并排序

归并排序是建立在归并操作上的一种有效的排序算法,该算法采用分治法(Divide and Conquer)。将已有序的子序列合并,得到完全有序的序列;即先保证子序列有序,然后将两个子序列合并成一个有序序列,称为二路归并。核心步骤:先分解、后合并。

归并递归版本

// 归并排序递归

void MergeSort(int* a, int n)
{
	int* tem = (int*)malloc(sizeof(int) * n);
	if (tem == NULL)
	{
		printf("malloc failed");
		exit(0);
	}

	_MergeSort(a, 0, n - 1, tem);

	free(tem);
}
void _MergeSort(int* a, int begin, int end, int* tem)
{
	//begin和end左闭右闭区间
	if (begin >= end) //只有一个元素就不需要归并了
	{
		return;
	}

	//1.分解
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tem);
	_MergeSort(a, mid+1, end, tem);

	//2.合并(都是闭区间)
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int j = begin;
	//开始合并
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tem[j++] = a[begin1++];
		}
		else
		{
			tem[j++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tem[j++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tem[j++] = a[begin2++];
	}

	//合并好一部分就拷贝回去,左闭右闭加1
	memcpy(a + begin, tem + begin, sizeof(int) * (end - begin + 1));

}

归并非递归版本

因为归并的递归类似于后续遍历,因此不适合用栈来模拟实现,会导致丢失区间问题。因此我们考虑使用循环来修改。利用gap来遍历数组,模拟二路归并的过程,注意区间的控制问题(很棘手)

归并一次后同意合并:

// 归并排序非递归
//后序遍历用栈不好改非递归
//考虑用循环
//第一轮,一个一个排,第二轮两个两个排,
void MergeSortNonR(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int* tem = (int*)malloc(sizeof(int) * n);
	assert(tem);
	memset(tem, 0, sizeof(int) * n);
	//排序
	int gap = 1;
	while (gap < n)
	{
		//printf("gap = %d: ", gap);
		for (int i = 0; i < n; i = i + 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = begin2 + gap - 1;

			//防止越界,修正边界
			//end1越界
			if (end1 >= n)
			{
				//把end1修成最后一个数
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if(begin2 >= n)// begin2越界
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n) // end2越界
			{
				end2 = n - 1;
			}



			//printf("[%d,%d] [%d,%d]->", begin1, end1, begin2, end2);
			//往tem,排序
			int j = i;//比较位置开始,即从begin1
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tem[j++] = a[begin1++];
				}
				else
				{
					tem[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tem[j++] = a[begin1++];

			}
			while (begin2 <= end2)
			{
				tem[j++] = a[begin2++];
			}
	
		}
		//排完全部的再拷贝
		//合并
		//printf("
");
		memcpy(a, tem, sizeof(int) * (end - begin + 1));
		gap = gap * 2;
	}

}

归并一个区间就合并:

// 归并排序非递归
//后序遍历用栈不好改非递归
//考虑用循环
//第一轮,一个一个排,第二轮两个两个排,
void MergeSortNonR(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int* tem = (int*)malloc(sizeof(int) * n);
	assert(tem);
	memset(tem, 0, sizeof(int) * n);
	//排序
	int gap = 1;
	while (gap < n)
	{
		//printf("gap = %d: ", gap);
		for (int i = 0; i < n; i = i + 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = begin2 + gap - 1;

			//防止越界,修正边界
			//end1越界
			if (end1 >= n)
			{
				//把end1修成最后一个数
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)// begin2越界
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n) // end2越界
			{
				end2 = n - 1;
			}
			//记录每次归并几个,就拷贝几个
			int count = end2 - begin1 + 1;

			//printf("[%d,%d] [%d,%d]->", begin1, end1, begin2, end2);
			//往tem,排序
			int j = i;//比较位置开始,即从begin1
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tem[j++] = a[begin1++];
				}
				else
				{
					tem[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tem[j++] = a[begin1++];

			}
			while (begin2 <= end2)
			{
				tem[j++] = a[begin2++];
			}
			//排完一部分就拷贝
			memcpy(a + i, tem + i, sizeof(int) * count);

		}

		gap = gap * 2;
	}

}

特性总结
1.缺点空间复杂度为O(N),该算法主要用于外部排序的思想。
2.时间复杂度:O(NlogN)
3.空间复杂度:O(N)
4.稳定性:稳定

6.计数排序

计数排序又称鸽巢原理,类似于哈希定址法:第一次统计数值出现的次数,根据统计的结果将序列回收到原来的序列中。

//计数排序
void CountSort(int* a, int n)
{
	//找出数据的范围,找出最大值与最小值然后做差,就是数组的范围
	int max = a[0], 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 count = max - min + 1;
	int* tem = (int*)malloc(sizeof(int) * count);
	assert(tem);
	memset(tem, 0, sizeof(int) * count);

	//遍历数组记录数据出现次数
	for (int i = 0; i < n; i++)
	{
		tem[a[i] - min]++;
	}

	int j = 0;
	//排序,tem中出现几次就往a写几个
	for (int i = 0; i < count; i++)
	{
		//只要tem[i]不为0,就会进入循环
		while (tem[i]--)
		{
			a[j++] = i + min;
		}
	}
}

特性总结
1.计数排序在数据范围集中时,效率很高,但不适合浮点数,数据范围相差太大的数据。
2.时间复杂度:O(max(N,range))
3.空间复杂度:O(range)
4.稳定性:稳定

以上就是本文对排序知识的总结,如有问题,欢迎在评论区讨论。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。