从这一节开始,我们正式开始总结和学习算法相关内容。

算法系列的文章源代码将陆续更新到github上,其地址如下:

https://github.com/zhaolingxi/AlgorithTtraining

由于排序的内容量较大,这一小篇文章主要介绍冒泡、选择、插入、归并、快速、堆,这几种排序的排序思想和实例代码。至于拓展部分的内容,则会单独在排序总结--拓展系列文章中展示。

所有的代码均由C++实现

在开始之前,我们默认需要了解基本的数据结构、基本的stl内容和基本的c++语法。

除此之外,我们引入算法稳定性的概念(时间、空间复杂度已经在上一篇文章中谈及)

算法的稳定性:此时算法的稳定性并不是针对复杂度的稳定性,而是针对使用完该算法之后,数据之间是否还有稳定性,(如原始数据中就有4个2,排完之后这4个2之间的次序是否还是一致的)

我们以实际的算法举例:冒泡排序在相等时不交换,可以做到稳定;插入排序在相等时同一规则,也可以做到稳定;归并排序时,在merge的时候,先拷贝左边的,就可以得到稳定的算法;快排由于小于区域和大于区域的值交换问题,是没有办法做到稳定的;堆排序就不用说了,父子关系并不一定相邻,根本不可能稳定

冒泡排序代码与思想:

//冒泡排序【给值找下标】
//时间复杂度:O(n^2)
//空间复杂度:O(1)
//算法稳定度:稳定
//排序思想:一次选择一个列表中的值(注意是值)
//	将这个值沉底或者冒出,一次确定一个最大值(沉底)或者最小值(冒出)
bool sort_Bubble(std::vector<int>& ioArray)
{
	bool res = false;
	if (ioArray.size() < 2) return res;
	for (size_t i = 0; i < ioArray.size(); i++)
	{
		for (size_t j=0; j < (ioArray.size()-i-1); j++)
		{
			if (ioArray[j]> ioArray[j+1])//下沉冒泡
			{
				Swap(ioArray[j], ioArray[j + 1]);
			}
		}
	}
	res = true;
	return res;
}

选择排序代码与思想:

//选择排序【给下标找值】
//时间复杂度:O(n^2)
//空间复杂度:O(1)
//算法稳定度:无
//排序思想:一次选择一个列表中的下标对应元素(可以从左到右选择)
//	将它假设为最小(或者最大),和剩下的元素比较,从而确定这一个下标对应的
//	元素值
bool sort_Selection(std::vector<int>& ioArray)
{
	bool res = false;
	if (ioArray.size() < 2) return res;
	for (size_t i = 0; i < ioArray.size(); i++)
	{
		for (size_t j = i+1; j < ioArray.size(); j++)
		{
			if (ioArray[i]> ioArray[j])
			{
				Swap(ioArray[i], ioArray[j]);
			}
		}
	}
	res = true;
	return res;
}

插入排序代码与思想:

//插入排序
//时间复杂度:O(n^2)
//空间复杂度:O(1)
//算法稳定度:稳定
//排序思想:从一个最小的有序数列开始(只有一个元素一定有序)
//	 逐步扩大这个数列,将后纳入的元素作为插入元素
//	 按序比较(小数列有序),给插入元素一个位置
bool sort_Insert(std::vector<int>& ioArray)
{
	bool res = false;
	if (ioArray.size() < 2) return res;
	for (size_t i = 1; i < ioArray.size(); i++)//长度为1的序列不要排序
	{
		for (size_t j = i; j >0; j--)
		{
			if (ioArray[j]<ioArray[j-1])
			{
				Swap(ioArray[j], ioArray[j-1]);
			}
		}
	}
	res = true;
	return res;
}

归并排序代码与思想:

//归并排序
//时间复杂度:O(nlogn)
//空间复杂度:O(n)
//算法稳定度:稳定
//排序思想:使用分区规划的思想【按下标进行分区】
//	默认划分的节点选为中点
//	一直划分到数组自然有序,即数组只有一个数的时候
//	使用merge方法,将有序的子数列合并为有序的大数列
bool sort_Merge(std::vector<int>& ioArray)
{
	bool res = false;
	if (ioArray.size() <2) return res;
	processMerge(ioArray, 0, (ioArray.size() - 1));
	res = true;
	return res;
}

int processMerge(std::vector<int>& ioArray, int left, int right)
{
	if (left==right) return ioArray[left];//直到不可拆分为止
	int mid = left +((right - left) >> 1);//防止溢出
	int leftMax = processMerge(ioArray, left, mid);
	int rightMax = processMerge(ioArray,mid+1,right);
	merge(ioArray,left,mid,right);
	return __max(leftMax,rightMax);
}

void merge(std::vector<int>& ioArray, int left, int mid, int right)
{
	std::vector<int> tempvec(right- left+1);
	int i = 0;
	int p1 = left;
	int p2 = mid + 1;
	while (p1<=mid && p2<=right)
	{
		tempvec[i++]=(ioArray[p1]<=ioArray[p2]?ioArray[p1++]:ioArray[p2++]);
	}
	while (p1<=mid)//右边已经跑完了
	{
		tempvec[i++] = ioArray[p1++];
	}
	while (p2<=right)//左边已经跑完了
	{
		tempvec[i++] = ioArray[p2++];
	}
	for (size_t i = 0; i < tempvec.size(); i++)
	{
		ioArray[left+i] = tempvec[i];
	}
}

快速排序代码与思想:

//快速排序
//时间复杂度:O(nlogn)
//空间复杂度:O(logn)
//算法稳定度:无
//排序思想:使用分区规划的思想【按值进行分区】
//	先选定一个元素作为分隔符,小于它的划进小于区,大于它的划进大于区
//	1.0版本中会默认将最后一个数作为划分的元素,然后将它和大于区域的第一个元素交换【O(n^2)】
//	2.0版本中会默认将最后一个数作为划分的元素,划分为大于区域、等于区域、小于区域三个区域【O(n^2)】
//	3.0版本中会加入随机值,避免最坏情况的发生(当原来的数列已经有序,拿后面的数划分,就没有最大区域)
//	简而言之,划分值打的偏了,划分就没有了意义,和没有划分一样了
//	在最终的算法中,我们掺入随机值来避免最坏情况的发生,即随机选择划分值
//	快排的空间复杂度取决于它的递归层数
//	快排的所有复杂度都是期望的结果
bool sort_Quick3(std::vector<int>& ioArray)
{
	bool res = false;
	if (ioArray.size() <2) return res;
	processQuick3(ioArray, 0, ioArray.size()-1);
	res = true;
	return res;
}

void processQuick3(std::vector<int>& ioArray, int left, int right)
{
	if (left<right)
	{
		Swap(ioArray[getRandomNum(left, right)],ioArray[right]);//选一个随机数放在右侧
		std::vector<int> tempvec;
		tempvec=partition(ioArray,left,right);
		processQuick3(ioArray, left, tempvec[0] - 1);//小于区域
		processQuick3(ioArray, tempvec[1] +1, right);//大于区域
	}
}

std::vector<int> partition(std::vector<int>& ioArray, int left, int right)
{
	int less = left - 1;//小于区域右边界
	int more = right;//大于区域左边界
	while (left<more)
	{
		if (ioArray[left]< ioArray[right])//ioArray[right] is base number
		{
			Swap(ioArray[++less],ioArray[left++]);
		}
		else if (ioArray[left] > ioArray[right])
		{
			Swap(ioArray[--more], ioArray[left]);
		}
		else
		{
			left++;
		}
	}
	Swap(ioArray[more],ioArray[right]);
	return (std::vector<int>{ less+1,more });
}

堆排序代码与思想:

//堆排序
//时间复杂度:O(nlogn)
//空间复杂度:O(1)
//算法稳定度:无
//	排序思想:还是使用了分治的思想,同时利用了堆的二叉树结构
//	我们人为构建一个大根堆或者小根堆,每一次调整完结构根就是最大(最小)
//	我们只需要不断的去维护这个结构就可以,维护这个结构不用动所有节点
//	所以它的复杂度还可以
bool sort_Heap(std::vector<int>& ioArray)
{
	bool res = false;
	if (ioArray.size() < 2) return res;
	for (size_t i = 0; i < ioArray.size(); i++)
	{
		heapinsert(ioArray,i);//整体大根堆
	}
	int heapsize = ioArray.size();
	Swap(ioArray[0], ioArray[--heapsize]);//把根和最后一个数交换【此时已经是大根堆】
	while (heapsize>0)
	{
		heapify(ioArray,0,heapsize);//每一个数向下沉【主要就是为了下一步做准备】
		Swap(ioArray[0], ioArray[--heapsize]);//之后再把最后一个数和根交换
	}
	res = true;
	return res;
}

void heapify(std::vector<int>& ioArray, int index, int heapsize)
{
	int left = index * 2 + 1;//左子树下标,乘2表示向下一层
	while (left<heapsize)//自己还有孩子
	{
		//比较左右孩子的值
		int largest = ((left + 1 < heapsize) && (ioArray[left + 1] > ioArray[left])) ? left + 1 : left;
		//比较孩子和父,谁配得上largest、
		largest = ioArray[largest] > ioArray[index] ? largest : index;
		if (largest==index)
		{
			break;
		}
		Swap(ioArray[largest], ioArray[index]);
		index = largest;
		left= index * 2 + 1;
	}
}

void heapinsert(std::vector<int>& ioArray, int index)
{
	while (ioArray[index]>ioArray[(index-1)/2])
	{
		Swap(ioArray[index], ioArray[(index - 1) / 2]);
		index = (index - 1) / 2;
	}
}

以上是总结的几种排序的算法和其思想,但是最为重要的部分还是理解其结构

最后,我们引入

master公式:

$$ T(N)=a*T(N/b)+O(N^d) $$

其中N代表规模,子问题规模为(N/b),子问题调用次数为a,其他的过程复杂度为O(N^d)

log(b,a)>d -->复杂度为O(N^log(b,a))

log(b,a)=d -->复杂度为O(N^d * log N)

log(b,a)<d -->复杂度为O(N^d )

Q.E.D.


世界上只有一种英雄主义