排序算法总结

1 综述

1.1 稳定性

比如A和B的数值相同,但未排序的时候A在B的前面,此时进行排序:
若排序后A依然在B前,则为稳定排序;
若排序后B跑到了A前,则为不稳定排序;
只要有一组发生了这样的情况,就是不稳定的排序了。
稳定的排序算法:归桶插计冒(简记为:鬼瞳插鸡毛??哈哈)

1.2 复杂度

时间复杂度:排序的两种操作:尽可能少的比较次数和尽可能少的移动次数;
空间复杂度:其他存储空间,临时空间等;
算法复杂性:冒泡、选择、插入,这三种属于简单算法;改进算法分别有希尔、堆、归并、快排。

低端的排序算法(不能突破O(n²) 的算法):

排序算法最好时间复杂度最差时间复杂度平均时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(n²)O(n²)O(1)稳定
插入排序O(n)O(n²)O(n²)O(2)稳定
选择排序O(n²)O(n²)O(n²)O(2)不稳定

进化的排序算法 O(n²) ~ O(nlogn) :

排序算法最好时间复杂度最差时间复杂度平均时间复杂度空间复杂度稳定性
带终止标志的改良冒泡排序O(n)O(n²)O(n²)O(2)稳定
带跳跃数的插入排序:希尔排序O(n1.3)O(n²)O(nlogn)O(2)不稳定
带备忘录的选择排序:堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定

高端的排序算法(用空间换时间的O(nlogn)) :

排序算法最好时间复杂度最差时间复杂度平均时间复杂度空间复杂度稳定性
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
快速排序O(nlogn)O(n²)O(nlogn)O(logn)不稳定

2 具体排序算法

2.1 冒泡排序

1、思想:两两比较相邻记录,反序则交换,将后面的小值一点点浮到前面达到有序(冒泡模型);
2、相邻:每次比较的两个都是相邻的;
3、后面的小值:应从后向前遍历;
4、复杂度分析:无终止条件标志,故O(n²),空间复杂度在交换操作处。

2.2 插入排序

1、思想:每次处理一个数字,顺序不对时,将前面有序的比他大的数字都往后挪一下,然后将它插入到应在的位置(扑克牌抓牌模型);
2、顺序不对时:当他的值比前一个值大的时候,才进入第二层循环;
3、前面有序:每次处理完一个数字时。此数字及前面的数字都是有序的;
4、往后挪:第二层循环是部分有序数字的整体向后走一下;
5、复杂度分析:移动和比较次数都相对较少,比前面两种算法时间复杂度低一些。空间复杂度在交换操作处有O(1),存待插入的最小值处还有O(1)。

2.3 选择排序

1、思想:每次找到最小的值,将其该次循环放在最前面的位置(群2炮灰模型);
2、每次找到最小:需要一个额外的空间来存储最小值,当然存最小值的下标更好;
3、放在最前面:最小的下标和当次循环最前面的下标交换。
4、复杂度分析:比较操作无论是否有序都是O(n²),但交换操作其实很少,虽然看起来最优时间复杂度很不好,但一般来说性能还好于冒泡排序;空间复杂度在交换操作处有O(1),存最小值下标处还有O(1)。


2.4 进化的冒泡排序:增加终止条件

1、思想:两两比较相邻记录,反序则交换,将后面的小值一点点浮到前面,直到再无反序
2、比起冒泡排序,多了一句“直到再无反序”:即需要有个终止条件标志。
3、此终止标志在已经有序时生效,跳出排序过程。
4、复杂度分析:有终止条件标志,故最好情况即已经有序,为O(n),空间复杂度在交换操作处有O(1),一个判断标志O(1)。

2.5 进化的插入排序:希尔排序

1、思想:跳跃式的插入排序,找到前面几个的那个数字,若大则与它交换,做到基本有序多次插入排序后最终有序。
2、跳跃式:不再是一步步的挪,将插入排序的循环中1变成一个称为增量的变量;
3、前面几个:就是增量,增量是人为设计的,影响性能;每次循环都要减小增量,到1时则已经有序;
4、基本有序:每次循环一次都能做到小的基本前面,大的基本都在后面
5、多次插入排序:增量的减小过程导致,在基本的插入排序外面还需要一层do-while语句。
6、复杂度分析:突破了O(n²)的限制,一般情况为O(n1.5),空间复杂度在交换操作处有O(1),一个判断标志O(1)。

2.6 进化的选择排序:堆排序

1、思想:为了避免前一次排序进行过的比较,借助大顶堆(也叫最大堆),通过虚拟的二叉树结构(本质还是个数组)进行选择排序。将其按广度优先遍历存入数组构成堆数组,随后只需要将堆顶与堆数组末尾元素交换位置,排除掉末尾元素完成即可排序。
2、虚拟的二叉树:其实只是个数组,但假想成二叉树的结构,只利用了完全二叉树种节点n的孩子结点是2n和2n+1这一条性质进行的;
3、堆数组:每次把最大值放在数组最前面的位置,看似有堆的性质;
4、堆顶与堆数组末尾元素:每次把堆顶也就是最大值与数组的最后一个元素交换位置,即最大值放在了最后;
5、排除掉末尾元素:最大值在最后了,下一轮找次最大值放在倒数第二个位置即可,因此完成一轮,最后的下标-1;
6、复杂度分析:第一次构建堆的度咋读为O(n),后面重建堆需要O(logn)的时间。故时间复杂度O(nlogn)。空间上只需要一个暂存最小下标数值的内存,故O(1)。


2.7 归并排序

归并排序思想:将序列分成长度为2或1的小序列,两两归并,继续两两归并,直到有序。(倒二叉树模型)
递归调用Msort分组,分组后调用Merge合并。

2.8 快速排序

快速排序思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小。然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
递归调用Qsort处理大小两部分序列,其中调用Partition找到具体下标位置。

3 其他非比较排序方式

3.1 计数排序

准备1个数组,把利用元素的实际值作为key,确定它们在输出数组中的位置,遍历新数组即可。

3.2 基数排序

准备10个数组。先把所有数据的个位扔进去,然后遍历这10个数组的每个值,第一轮排序结束;然后遍历十位,以此类推。

3.3 桶排序

把数组的区间分成若干个桶,然后分别排序。

4 参考代码

#include"sort.h"

/
// 冒泡排序 //
/

void BubbleSort0(vector<int>& L)//虚假的冒泡排序
{
	for (int i = 0; i < L.size(); i++)
	{
		for (int j = 0; j < i; j++)
		{
			if (L[j] > L[i])//每一个关键字都与后面的每个关键字比较,并不是比较的相邻记录
			{
				swap(L[j], L[i]);
			}
		}
	}
}

void BubbleSort1(vector<int>& L)//正宗的冒泡排序
{
	for (int i = 0; i < L.size(); i++)
	{
		for (int j = L.size() - 1; j > i; j--)//正宗的原因:第二层循环的j是从后往前循环
		{
			if (L[j - 1] > L[j])//较小的数字从底下慢慢浮到上面
			{
				swap(L[j - 1], L[j]);
			}
		}
	}
}

void BubbleSort2(vector<int>& L)//进一步优化的冒泡排序
{
	bool FLAG = true;//改进1:初始化一个标记
	for (int i = 0; i < L.size() && FLAG; i++)//改进2:如果FALG为false,则提前跳出循环,避免有序时做无意义的循环判断
	{
		FLAG = false;//改进3:此标记在新一层循环初始化为false
		for (int j = L.size() - 1; j > i; j--)
		{
			if (L[j - 1] > L[j])
			{
				swap(L[j - 1], L[j]);
				FLAG = true;//改进4:如果有数据交换,才令flag为true,判断是否有数据交换
			}
		}
	}
}


/
// 选择排序 //
/

void SelectSort(vector<int>& L)//选择排序
{
	int min;//min是一个下标,用下标可以避免初始化的问题。选择排序的要点:每次找到最小值的下标
	for (int i = 0; i < L.size(); i++)
	{
		min = i;//i是每次最小值应当放的位置
		for (int j = i + 1; j < L.size(); j++)//j从i+1开始即可,不必从i开始
		{
			if (L[min] > L[j])//如果值更小,则更新最小值的下标
			{
				min = j;
			}
		}
		if (i != min)//这个判断很鸡肋?用一个判断操作来减少多余的赋值操作
		{
			swap(L[i], L[min]);
		}	
	}
}

/
// 插入排序 //
/

void InsertSort(vector<int>& L)//插入排序
{
	for (int i = 1; i < L.size() ; i++)
	{
		if (L[i] < L[i - 1])
		{
			int j;
			int insert = L[i];
			for (j = i - 1; L[j] > insert && j >= 0; j--)
			{
				L[j + 1] = L[j];
			}
			L[j + 1] = insert;
		}
	}
}

/
// 希尔排序 //
/

void ShellSort(vector<int>& L)//希尔排序:改良的插入排序
{
	int increment = L.size();//增量序列:大量数据进行分组

	do
	{
		increment = increment / 3 + 1;//增量序列:不是一步一步的挪动,而是跳跃式的向前插入。这里的3如何确定是个数学难题

		for (int i = increment ; i < L.size(); i++)//这里的for就是个插入排序,把所有的1变成了increment而已
		{
			if (L[i] < L[i - increment])
			{
				int j;
				int insert = L[i];//插入的变成了有序增量子表
				for (j = i - increment; L[j] > insert && j >= 0; j -= increment)
				{
					L[j + increment] = L[j];
				}
				L[j + increment] = insert;
			}
		}
	} while (increment > 1);//当增量大于1的时候就继续进行
}

/
/// 堆排序 ///
/

void HeapAdjust(vector<int>& L, int s, int m);//声明需要用的函数HeapAdjust
/*假设下标s到m的关键字除了s下标的位置都满足了堆的定义,调整L[s]的关键字,使其成为大顶堆*/

void HeapSort(vector<int>& L)//堆排序:改良的插入排序。构成一个大顶堆,随后把最大的值放在最后,然后排除掉最后一个元素递归
{
	for (int i = (L.size() - 1)/2; i >= 0; i--)
	{
		HeapAdjust(L,i,L.size() - 1);//从数组的一半开始(因为要从二叉树的非叶子结点找),循环把L构成一个大顶堆(结点调整过程)
	}

	for (int i = L.size() - 1; i > 0; i--)//不用≥0因为最后一步可以不操作
	{
		swap(L[0], L[i]);//将最大堆的顶部放进数组最后一个位置,同时堆顶设为数组最后一个位置的值
		HeapAdjust(L,0, i - 1);//不管最后一个位置,重新调整成大顶堆,继续处理前面的位置
	}
}

void HeapAdjust(vector<int>& L, int s, int m)//s是下标最小的值,m是下标最大的值,从s到m就是下标范围
{
	int temp = L[s];//temp寄存下标最小下标的数值
	
	for (int j = 2 * s ; j <= m; j *= 2)//这里是在找s的孩子结点(完全二叉树的结点n的孩子结点是2n和2n+1),沿关键字较大的孩子结点向下筛选
	{
		if (j < m && L[j] < L[j + 1])//j<m是为了不超范围,L[j]是左孩子的值,L[j+1]是右孩子的值,j保存着两个孩子大的那个值的下标
		{
			++j;
		}
		if (temp >= L[j])//如果当前结点比两个孩子都大,就跳出循环
		{
			break;
		}

		L[s] = L[j];//如果不是当前结点比两个孩子都大,把数值换一下,
		s = j;//如果不是当前结点比两个孩子都大,同时也把下标换一下,
	}

	L[s] = temp;//把最小下标的数值存为最大的值
}


//
/// 归并排序 /
/

void Msort(vector<int>& data, vector<int>& copy, int start, int end);//子数组排序过程
void Merge(vector<int>& data, vector<int>& copy, int start, int length, int end);//两个数组合并过程

void MergeSort(vector<int>& L)
{
	vector<int> copy;//需要整体复制一下全部的数组,这里开辟空间
	for (int i = 0; i < L.size(); i++)
	{
		copy.push_back(L[i]);
	}
	Msort(L, copy, 0, L.size() - 1);//把数组封装成一个四参数的函数,用于后面的递归(0是开始位置,L.size()-1是结束位置)
}

void Msort(vector<int>& data, vector<int>& copy, int start, int end)
{
	if (start == end)
	{
		copy[start] = data[start];//copy:在Msort阶段寄存归并后的数组,在Merge阶段合并存回原数组data中
		return;
	}

	int length = (end - start) / 2;					//将数组平分成两部分,得到每部分的长度
	Msort(copy, data, start, start + length);		//将前一半数组排序,并存在copy[start ... start+length]中
	Msort(copy, data, start + length + 1, end);		//将后一半数组排序,并存在copy[start+length+1 ... end]中
	Merge(data, copy, start, length, end);			//将前后两个排序数组合并,存在data[start+length+1 ... end]中
}

void Merge(vector<int>& data, vector<int>& copy, int start, int length, int end)//两个数组合并过程
{
	int i = start + length;		//原数组前一半的最后一个索引
	int j = end;				//原数组后一半的最后一个索引
	int newIndex = end;			//data数组最后一个索引

	while (i >= start && j >= start + length + 1)//归并过程:从后向前
	{
		if (copy[i] > copy[j])
		{
			data[newIndex] = copy[i];
			newIndex--;
			i--;
		}
		else
		{
			data[newIndex] = copy[j];
			newIndex--;
			j--;
		}
	}
	while (i >= start)//处理没有归并进去的剩余部分(最小的一些数字)
	{
		data[newIndex] = copy[i];
		newIndex--;
		i--;
	}
	while (j >= start + length + 1)//处理没有归并进去的剩余部分(最小的一些数字)
	{
		data[newIndex] = copy[j];
		newIndex--;
		j--;
	}
}

//
/// 快速排序 /
/

void Qsort(vector<int>& data, int length, int start, int end);
int Partition(vector<int>& data, int length, int start, int end);//把比这个数字大的都放在这个数字下标的后面,比这个数字小的都放在这个数字下标的前面,返回这个下标。

void QuickSort(vector<int>& L)
{
	Qsort(L, L.size(), 0, L.size() - 1);//把数组封装成一个四参数的函数,用于后面的递归(0是开始位置,L.size()-1是结束位置)
}

void Qsort(vector<int>& data, int length, int start, int end)
{
	if (start == end)//终止条件:只剩一个数字
		return;

	int index = Partition(data, length, start, end);//把比这个数字大的都放在这个数字下标的后面,比这个数字小的都放在这个数字下标的前面,返回这个下标。

	if (index > start)
		Qsort(data, length, start, index - 1);//递归:在这堆数字前面的小数子数组,进行递归切分
	if (index < end)
		Qsort(data, length, index + 1, end);//递归:在这堆数字前面的大数子数组,进行递归切分
}

int Partition(vector<int>& data, int length, int start, int end)//start是起始位置下标,end是终止位置后面的坐标,length = data.size()只是判断时调用
{
	if (length <= 0 || start < 0 || end >= length)//判断异常情况
	{
		throw new std::exception("Invalid Parameters");
	}

	int index = (start + end) / 2;//这个可以是任意值,只要在start和end之间

	swap(data[index], data[end]);===在数组中选择的那个数字暂存在最后一位

	int small = start - 1;//small是选出的那个数字,排序后应该所在的位置下标

	for (int i = start; i < end; ++i)//进行遍历:small的递增找到排序后应该所在的位置下标,Swap将比选出的值小的数放在前面
	{
		if (data[i] < data[end])
		{
			++small;
			if (small != i)
			{
				swap(data[i], data[small]);//把小值放在“排序后应该所在的位置下标”的前面
			}
		}
	}
	small++;

	swap(data[small], data[end]);===暂存在最后一位的那个数字换到应在的位置

	return small;
}


//
/// 链表归并排序 /
/

#if 链表排序
class ListNode
{

public:
	int val;
	ListNode* next;
	ListNode(int x): val(x), next(nullptr){}
};

void create(ListNode* guard)
{
	ListNode* p = guard;
	p->next = new ListNode(1); p = p->next;
	p->next = new ListNode(5); p = p->next;
	p->next = new ListNode(9); p = p->next;
	p->next = new ListNode(8); p = p->next;
	p->next = new ListNode(5); p = p->next;
	p->next = new ListNode(2); p = p->next;
	p->next = new ListNode(3); p = p->next;
	p->next = new ListNode(6); p = p->next;
	p->next = new ListNode(4); p = p->next;
	p->next = new ListNode(7); p = p->next;
}

void merge(ListNode* guard, ListNode* middle)
{
	ListNode* p = guard;
	ListNode* l1 = guard->next;
	ListNode* l2 = middle->next;
	middle->next = nullptr;
	while (l1 != nullptr && l2 != nullptr)
	{
		if (l1->val < l2->val)
		{
			p->next = l1;
			l1 = l1->next;
		}
		else
		{
			p->next = l2;
			l2 = l2->next;
		}
		p = p->next;
	}
	p->next = nullptr;
	if (l1 != middle->next)
		p->next = l1;
	if (l2 != nullptr)
		p->next = l2;
	
}
void mergeSort(ListNode* guard, ListNode* last)
{
	if (last == guard->next)
		return;
	ListNode* walker = guard;
	ListNode* runner = guard;
	while (runner != last)
	{
		walker = walker->next;
		runner = runner->next;
		if(runner != last)
			runner = runner->next;
	}
	ListNode* middle = walker;

	mergeSort(guard, middle);
	mergeSort(middle, last);

	merge(guard, middle);
}
int main()
{
	ListNode* guard = new ListNode(0);
	create(guard);
	ListNode* p = guard;
	while (p->next)
	{
		p = p->next;
	}
	ListNode* last = p;
	
	mergeSort(guard, last);

	return 0;
}
#endif

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值