排序

138 阅读11分钟

直接插入排序

--思路

将待排序的元素,逐一插入到有序序列中,直到所有元素插入完为止.

初始的有序序列只有第一个元素.

image.png

--代码实现

单次插入

用一个变量end来标识每次插入前的有序序列,[ 0, end ]为有序序列.

将end + 1位置的值insertEle插入到前面,

把比insertEle大的元素往后挪,同时也能找到插入的位置.

多次插入

多次改变end的初始值,也能理解为待插入的元素数目.

void insertSort(vector<int>& v)
{
	for (int i = 0; i < v.size() - 1; ++i) // 表示要插入的元素数目
	{
		//单次插入

		//[0,end]有序,将end+1位置的值插入到前面
		int end = i;
		int insertEle = v[end + 1];

		//用end反向遍历有序序列(升序为例),挪动数据并找到插入位置
		//比insertEle大的数都往后挪动
		while (end >= 0)
		{
			if (insertEle < v[end])
			{
				v[end + 1] = v[end];
				--end;
			}
			else
				break;
		}
		//此时插入位置为end+1
		v[end + 1] = insertEle;
	}
}

--时间复杂度

最坏情况: 逆序

第一个数据挪0次,第二个数据挪1次,……第n个数据挪动n-1次.

因此是O(N^2)

最优情况: 有序/接近有序

挪动次数少,O(N)

希尔排序

希尔排序是对直接插入排序的优化,时间复杂度是O(N*logN).

--思路

直接插入排序在 有序/接近有序 时,效率很高.

希尔排序分2步:

(1) 预排序 —— 让数据接近有序

(2) 直接插入排序

注意:这里的预排序会进行多次

--预排序

步骤

(1) 将数据分为gap组,间距为gap的倍数的元素在同一组

例:假设gap = 3,vector< int > v如下

image.png

(2) 对这gap组数据分别进行直接插入排序

image.png

这次预排序结束后,数据已经接近有序,

接下来使用直接插入排序只需挪动3次数据.

预排序gap的设置

(1) gap越大,数据一次挪动gap步

大的数更快到后面,小的数更快到前面,但越不接近有序

(2) gap越小,数据挪动慢,但越接近有序,

当gap = 1时,就是对所有元素进行直接插入排序.

(3) gap大了,不够接近有序;

gap小了,和直接插入排序差不多.

结论

(1) 需要多次预排序,让数据逐步接近有序

(2) gap开始大(与v.size()相关),逐步递减,让序列逐步接近有序

(3) 每一次预排序都会让数据更接近有序,除第一次预排外,

每一次预排序都能享受之前的预排序带来的效果,

使得即使后面gap逐步递减,数据挪动的次数也会变少.

--代码实现

单次插入

用变量end来标识当前组有序序列的结尾,

将v[ end+group ]插入到[0,end]的该组元素序列中

image.png

多次插入

gap组数据交替进行直接插入排序

end指向哪一组有序序列的结尾,就对这组进行一次插入.

image.png

void  shellSort(vector<int>& v)
{
	int gap = v.size()/2;
  //gap > 1时,都是预排序;
  //gap = 1,直接插入排序,此时数据已经 接近有序/有序
	while (gap >= 1)
	{
		for (int i = 0; i < v.size() - gap; ++i) //gap组数据交替进行直接插入排序
		{
			//当前组的[0,end]内的元素有序,将end + gap位置的值插入
			int end = i;
			int insertEle = v[end + gap];

			//单次插入
			//用end反向遍历当前组的有序序列
			//比insertEle大的数据往后挪gap步,同时找到插入位置
			while (end >= 0)
			{
				if (insertEle < v[end])
				{
					v[end + gap] = v[end];
					end -= gap;
				}
				else
					break;
			}
			v[end + gap] = insertEle;
		}
		//更新gap前,看当前gap的处理结果
		//cout << "gap:" << gap << "-> ";
		//printVector(v);

		gap /= 2;
	}
}

--时间复杂度

仅从理解方面分析,以gap = v.size()/2为例

(1) 最外层调整gap的循环,表示预排序的次数,O(logN).

(2) 当gap很大时:

此时2个数据为1组,里面的单次插入时间可以忽略不计,

所以gap组数据交替进行插入排序时,消耗O(N).

(3) 当gap很小时:

此时数据已经接近有序,里面的单次插入次数少,消耗O(N)

(4) 当gap为中间值时:

每一次预排序都会让数据更接近有序,除第一次预排外,

每一次预排序都能享受之前的预排序带来的效果,

使得即使后面gap逐步递减,数据挪动的次数也会变少.

因此也可以认为是O(N).

结论

所以预排序的时间复杂度大概是O(N*logN),希尔排序中的直接插入排序是O(N)

希尔排序的时间复杂度大概是O(N*logN).

直接选择排序

--思路

(升序为例)每次遍历待排序的元素,选出最小值,

和刚遍历的起始位置值做交换.

image.png

--代码实现

void seleteSort(vector<int>& v)
{
	//在[begin,v.size() - 1]区间选出最小的数,放到begin位置,接着++begin
	//直到这个区间只剩下一个元素,即begin == v.size() - 1时停止
	for (int begin = 0; begin < v.size() - 1; ++begin)
	{
		//先假设v[begin]最小

		//最小值下标(用于最后交换数据)
		int minIndex = begin;
		//对应的最小值(用于比较)
		int min = v[minIndex];

		//遍历begin之后的数
		//若比min小,记录它的下标和值
		for (int j = begin + 1; j < v.size(); ++j)
		{
			if (v[j] < min)
			{
				minIndex = j;
				min = v[j];
			}
		}

		//此时minIndex为最小值下标,把 最小值位置 和 begin位置 的值做交换
		swap(v[minIndex], v[begin]);
	}
}

--时间复杂度

无论是有序还是无序,时间复杂度均为O(N^2).

begin = 0时,内部循环遍历 N - 1个数据

begin = 1时,内部循环遍历 N - 2个数据

……………………………………………

begin = N - 2, 内部循环遍历 1 个数据

等差数列 1 + 2 + …… + (N-1) = O(N^2)

堆排序

--思路

回顾

堆是一种数据结构,是完全二叉树的一种存储结构(顺序存储结构).

大堆:根结点比左孩子和右孩子要大.

小堆:根结点比左孩子和右孩子要小.

排升序建大堆.

重点

堆能选出最大 (大堆) 或 最小 (小堆) 的数,放到堆顶,通过该特征实现堆排序.

--代码实现

//adjust:要向下调整的位置,adjust位置结点的左孩子和右孩子必须都是大堆
//end:堆的元素下标最大值[左闭右开)
void adjustDown(vector<int>& v, int adjust, int end)
{
	//待调整的结点有孩子,才可以向下调整
	while (adjust*2+1 < end)
	{
		//假设最大的孩子是左孩子
		int maxChild = adjust * 2 + 1;

		//如果右孩子存在且大于左孩子
		if (maxChild + 1 < end && v[maxChild + 1] > v[maxChild])
		{
			++maxChild;
		}

		//如果待调整结点小于最大的孩子,需要向下调整,否则直接结束
		if (v[maxChild] > v[adjust])
		{
			swap(v[maxChild], v[adjust]);
			adjust = maxChild;
		}
		else
			break;
	}
}

void heapSort(vector<int>& v)
{
	//1 建堆(向下调整建堆) ——  从最后一个非叶子结点开始建堆
	//即最后一个结点的父结点
	for (int i = ((v.size() - 1) - 1) / 2; i >= 0; --i)
	{
		adjustDown(v, i, v.size());
	}

	//  2 交换堆顶元素选数 + 调堆
	for (int i = v.size() - 1; i > 0; --i)
	{
		//把选出的最大值交换到末尾
		swap(v[i], v[0]);

		//将剩下的数调整成堆,
		adjustDown(v, 0, i); //这里是左闭右开,不会包含i
	}
}

--时间复杂度

回顾

一棵满二叉树,结点数N = 2^h-1(根结点高度为1时)

一棵完全二叉树,结点数一定 <= 2^h - 1,

同时一定 > 2^(h-1) - 1.

高度为h的完全二叉树,结点数一定比高度为h-1的满二叉树要大.

所以:h = O(logN)

建堆

image.png

将h = O(logN)代入,可知建堆循环的消耗是O(N).

选数+调堆

对n - 1个数调堆,向下调整消耗log(n-1)

对n - 2个数调堆,向下调整消耗log(n-2)

…………………………

对 2 个数调堆,向下调整消耗log 2

总消耗为:log(n-1) + log(n-2)+……+log2 = O(NlogN).

冒泡排序

--思路

遍历待排序序列,以升序为例

相邻的两个数进行比较,大的数交换到右边,小的数交换的左边.

每进行一次冒泡,可以选出当前待排序序列的最大值放到右边,待排序的元素数目-1.

--代码实现

void bubbleSort(vector<int>& v)
{
	//每一次冒泡都能让待排序序列的最大值放到末尾,待排序的元素数目-1
	//要进行v.size() - 1次冒泡
	for (int i = 0; i < v.size() - 1; ++i)//i表示完成的冒泡次数
	{
		//标识此次冒泡是否有发生交换
		bool flag = false;
		
		//其中一次冒泡
		//begin和begin+1位置的值比较,大的交换到右边
		for (int begin = 0; begin < v.size() - 1 - i; ++begin)//待排序序列的末尾会改变
		{
			if (v[begin] > v[begin + 1])
			{
				swap(v[begin], v[begin+1]);
				flag = true;
			}
		}

		//若此次冒泡没有发生交换,说明已经有序
		if (flag == false)
			return;
	}
}

--时间复杂度

最坏情况

第1次冒泡:遍历N-1个元素

第2次冒泡:N - 2

………………

第N-1次冒泡:1

1+2 + …… + (N-1) = O(N^2)

最好情况

有序,遍历一遍没有发生交换,则直接结束函数.

快速排序

--思路

以升序为例:

( 1 ) 任取 待排序序列 的某个元素作为基准值,将待排序序列分割成两个子序列.

左子序列中所有元素小于 基准值,右子序列中所有元素大于 基准值

( 2 ) 此时该基准值已经排到了正确的位置.

image.png

( 3 ) 然后左右序列,重复该过程(通过递归),直到所有元素排列在正确位置.

( 4 ) 快速排序是一种基于二叉树结构的排序方法,

基准值 —— 根

左区间 —— 左子树

右区间 —— 右子树

--分割成左右序列的三种方式

选出一个基准值key,一般最左边或最右边的值.

一趟快排完成后,左边序列比key小,右边序列比key大.

默认选择最左边的值做key.

hoare版本

目标

比key小的数放到左边,大的数放到右边

步骤

( 1 ) 定义left和right双下标

( 2 ) left 从左遍历,在左边找大

( 3 ) right 从右遍历,在右边找小

( 4 ) 交换left和right对应的值

image.png

( 5 ) left 继续找大,right继续找小

( 6 ) 当left和right相遇时,交换key与这个位置的值

【 必须保证相遇位置的值,小于key 】

image.png

代码:

返回基准值的新下标keyi,分割成两个区间:

[begin, keyi - 1] keyi [keyi + 1, end]

左边比key小,右边比key大

//快速排序单趟:hoare版本
//[begin, end]是要进行单趟排序的区间
int quickPartSortHoare(vector<int>& v, int begin, int end)
{
	//key位置
	int keyi = begin;
	//选区间第一个数为key
	int key = v[begin];
	
	while (begin < end)
	{
		//左边做key,右边先走(为了保证相遇位置的值,比key小)

		//右边找小,v[end]大就一直往前找
		while (begin < end && v[end] >= key)
			--end;

		//左边找大,v[begin]小就一直往后找
		while (begin < end && v[begin] <= key)
			++begin;

		//情况1:begin = end;
		//情况2:v[begin] > key且v[end] < key,即左边找到大,右边找到小
		swap(v[begin], v[end]);
	}

	//交换 key位置 和 相遇位置 的值
	swap(v[keyi], v[begin]);

	//更新keyi,返回基准值新下标
	keyi = begin;
	return keyi;
}

为什么左边做key,右边先走?

(right找小,left找大)

根本原因

左边做基准值:

说明相遇位置的值必须小于key,因为最后要交换相遇位置和基准值位置

情况分析

情况1:right先走,right没有找到小于key的数,right 去遇到 left

此时left的位置要么在keyi,要么就是小于key的值

(right找到小,left找到大后,要交换对应位置的值,所以此时left指向的值一定比基准值小)

情况2:right先走,但right找到小于key的数,停下来了,left 去遇到 right.

综上所述:左边做key,右边先走,能保证相遇位置的值一定比基准值小

挖坑法

与hoare版本的思路类似

步骤

( 1 ) 把左边选的基准值key,存放在临时变量中,此时keyi形成一个坑位

( 2 ) 定义left 和 right,left用来找比基准值大的数,right找比基准值小的数

( 3 ) 右边找小,找到后把数填进坑位,right位置形成坑位

( 4 ) 左边找大,找到后把数填进坑位,left位置又形成坑位

( 5 ) 当它们相遇时,一定在坑位上,此时把key填进坑位

不需要考虑【为什么左边做key,右边先走】,

key的位置keyi天然形成了一个坑位,只需要专注填坑.

代码

//快速排序单趟:挖坑法
int quickPartSortHole(vector<int>& v, int begin, int end)
{
	int key = v[begin];

	//基准值的位置形成坑
	int hole = begin;
	while (begin < end)
	{
		//右边找小
		while (begin < end&& v[end] >= key)
			--end;
		//找到以后,把值填进旧坑位,end位置形成新的坑位
		v[hole] = v[end];
		hole = end;

		//左边找大
		while (begin < end && v[begin] <= key)
			++begin;
		//找到以后,把值填进旧坑位,begin位置形成新的坑位
		v[hole] = v[begin];
		hole = begin;
	}

	//最后相遇在坑位
	v[hole] = key;
	return hole;
}

前后指针版本

步骤

( 1 ) 定义pre和cur前后下标

( 2 ) cur用于遍历

( 3 ) pre指向小于基准值的数,同时pre前面的数都是小于基准值的数(基准值本身除外)

( 4 ) 当cur遍历到的值小于基准值,++pre,然后把pre指向的值和当前cur指向的值做交换

( 5 ) 遍历完成后,pre位置的值和基准值位置的值做交换

image.png

代码

//快速排序单趟:前后指针版本
int quickPartSortPreCur(vector<int>& v, int begin, int end)
{
	//选择基准值
	int key = v[begin];
	
	//初始化前后指针
	int pre = begin;
	int cur = begin + 1;
	
	while (cur <= end)
	{
		if (v[cur] < key)
		{
			++pre;
			swap(v[pre], v[cur]);
		}
		++cur;
	}
	//此时pre前面的数(包括pre指向的数)都是小于基准值(除基准值外)
	swap(v[pre], v[begin]);
	//交换后基准值的位置就是pre
	return pre;
}

--递归代码实现

快排的递归实现,和二叉树的前序遍历类似.

void quickSortChild(vector<int>& v, int begin, int end)
{
	if (begin >= end)
		return;
	//单趟排序任选一种
	//int keyi = quickPartSortHoare(v, begin, end);
	//int keyi = quickPartSortHole(v, begin, end);
	
	//先处理 基准值,将基准值排到正确位置
	int keyi = quickPartSortPreCur(v, begin, end);
	
	//处理左区间和右区间
	//左区间:[begin, keyi - 1]
	//右区间:[keyi + 1, end]
	quickSortChild(v, begin, keyi - 1);
	quickSortChild(v, keyi + 1, end);
}
void quickSortR(vector<int>& v)
{
	quickSortChild(v, 0, v.size() - 1);
}

缺陷和改进策略

缺陷

基准值key的选择会影响效率:

如果每次选的key都是中位数,那每次都是二分,递归深度为O(logN)

如果每次选的key都是max或min,递归深度高且时间复杂度达O(N^2)

递归深度高达O(N),容易发生栈溢出

每次选key遍历剩下的数消耗为O(N),

时间复杂度高达O(N^2)

image.png

改进

(1) 三数取中

每次选到最小或最大,可能性不高.

第一个元素 中间元素 最后一个元素,在这三个元素中选出不是最大也不是最小的元素.

//三数取中,返回中间值的下标
int getMidKeyi(const vector<int>& v, int begin, int end)
{
	int mid = (begin + end) / 2;

	if (v[begin] < v[mid])
	{
		if (v[mid] < v[end])
			return mid;
		//否则mid位置为最大值
		else if (v[begin] < v[end])//次大值即为中间值
			return end;
		else
			return begin;
	}
	else//v[begin] > v[mid]
	{
		if (v[end] > v[begin])
			return begin;
		//否则begin位置为最大值
		else if (v[mid] < v[end])//找次大值
			return end;
		else
			return mid;
	}
}

单趟排序之前,将选出的中间数和第一个数互换位置,

仍然选第一个数作为key,例

int quickPartSortPreCur(vector<int>& v, int begin, int end)
{
  //三数取中,选出中间数,和begin位置的值交换
	int tmp = getMidKeyi(v, begin, end);
	swap(v[begin], v[tmp]);
	
	//选择基准值
	int key = v[begin];
	
	//初始化前后指针
	int pre = begin;
	int cur = begin + 1;
	
	while (cur <= end)
	{
		if (v[cur] < key)
		{
			++pre;
			swap(v[pre], v[cur]);
		}
		++cur;
	}
	//此时pre前面的数(包括pre指向的数)都是小于基准值(除基准值外)
	swap(v[pre], v[begin]);
	//交换后基准值的位置就是pre
	return pre;
}
void testGetMidKeyi()
{
	srand((unsigned)time(nullptr));
	
	//创建数据
	vector<int> v;
	//数据量
	int num = 10000;
	for (int i = 0; i < num; ++i)
		v.push_back(rand() % num);

	//先把数据排好序
	heapSort(v);

	//加入三数取中后,可以算出,若不加入程序会崩溃
	//clock()返回程序运行到调用位置所用的毫秒数
	clock_t begin = clock();
	quickSortR(v);
	clock_t end = clock();
	cout << "快排速度:" << end - begin << endl;
}

( 2 ) 小区间优化

当数据量大时,例对1000000个数据进行快排.

当其中一个子区间只有10个数据时,为了让这个子区间有序,仍然需要继续递归.

函数调用的消耗大.

为了让这些小区间有序,同时减少递归次数,

当区间比较小时,直接用其它排序(直接插入排序)处理小区间

void insertSort(vector<int>& v, int left, int right)
{
	for (int i = left; i < right; ++i)
	{
		//单次插入
		//[left, end]有序,将end + 1 的位置的值插入到前面
		int end = i;
		int insertEle = v[end + 1];
		while (end >= left)
		{
			if (insertEle < v[end])
			{
				v[end + 1] = v[end];
				--end;
			}
			else
				break;
		}
		v[end + 1] = insertEle;
	}
}

void quickSortChild(vector<int>& v, int begin, int end)
{
	if (begin >= end)
		return;
	//如果这个区间只有10个数,使用插入排序
	if (end - begin + 1 <= 10)
	{
		//写一个在[begin, end]区间进行插入排序的函数
		insertSort(v, begin, end);
		return;
	}
        
	int keyi = quickPartSortPreCur(v, begin, end);
	quickSortChild(v, begin, keyi - 1);
	quickSortChild(v, keyi + 1, end);
}

--非递归代码实现

用栈模拟递归过程.

递归的过程:先处理整个区间的 基准值,再去处理 左区间 和 右区间.

用一个栈(后进先出) 存储 要处理的区间

image.png

//非递归
void quickSortNotR(vector<int>& v)
{
	//存储区间
	stack<int> st;

	//[0, v.size() - 1]
	//栈后进先出,所以先入 区间右边
	st.push(v.size() - 1);
	st.push(0);

	while (!st.empty())
	{
		//当前区间出栈,准备处理
		int left = st.top();
		st.pop();
		int right = st.top();
		st.pop();

		//处理当前区间的基准值
		int keyi = quickPartSortPreCur(v, left, right);

		//将当前区间分为 左区间 和 右区间
		//[left,keyi - 1]  [keyi + 1, right]
		//区间存在且大于一个数,区间入栈
		if (right  > keyi + 1)
		{
			st.push(right);
			st.push(keyi + 1);
		}
		if (keyi - 1 > left)
		{
			st.push(keyi - 1);
			st.push(left);
		}
	}
}

--时间复杂度

最坏情况

每次选的基准值都为max或min,O(N^2)

最好情况

每次选的基准值都是中位数,O(N*logN).

归并排序

--思路

将待排序序列不断二分,直到得到有序的子序列(单个元素),

然后将已有序的子序列两两合并,得到有序序列.

image.png

--二路归并

将两个有序序列合并成一个有序序列

image.png

//将两段有序区间[begin1, end1] [begin2, end2]合并成一段,放入tmp中
void singleMerge(vector<int>& v, int begin1, int end1, int begin2, int end2, int* tmp)
{
	int i = 0;

	//任一区间数据放完就结束
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (v[begin1] <= v[begin2])
			tmp[i++] = v[begin1++];
		else if (v[begin2] < v[begin1])
			tmp[i++] = v[begin2++];
	}

	//然后把另一个区间剩下数据拷贝回去
	while (begin1 <= end1)
		tmp[i++] = v[begin1++];
	while (begin2 <= end2)
		tmp[i++] = v[begin2++];
}

--代码实现

无论是递归代码还是非递归代码,都需要一个临时空间tmp,

tmp的空间大小与待排序序列相同,tmp每个下标位置都能对应待排序序列.

子序列进行二路归并,合并到tmp对应位置中,再把归并完成的序列拷贝回原序列.

image.png

递归代码实现

tmp的位置与原序列的位置一一对应,所以归并到tmp中,数据的位置也必须在原来的范围.

例:[4, 5] 和 [6, 7] 位置的数据进行归并,放到tmp中

这些数据必须放在tmp的[4, 7]范围.

我通过手动传起始位置pos,保证数据归并到tmp的位置

//将两段有序区间[begin1, end1] [begin2, end2]合并成一段,放入tmp中,起始位置是pos
void singleMerge(vector<int>& v, int begin1, int end1, int begin2, int end2, int pos, int* tmp)
{
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (v[begin1] <= v[begin2])
			tmp[pos++] = v[begin1++];
		else if (v[begin2] < v[begin1])
			tmp[pos++] = v[begin2++];
	}

	while (begin1 <= end1)
		tmp[pos++] = v[begin1++];
	while (begin2 <= end2)
		tmp[pos++] = v[begin2++];
}

void mergeSortChild(vector<int>& v, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;

	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int pos = begin;

	//[begin1, end1]  [begin2, end2] 分治递归,让子区间有序
	mergeSortChild(v, begin1, end1, tmp);
	mergeSortChild(v, begin2, end2, tmp);

	//将两个有序区间归并到tmp中,起始位置为pos
	singleMerge(v, begin1, end1, begin2, end2, pos, tmp);

	//将归并好的tmp中数据 拷贝回原序列
	while (begin <= end)
	{
		v[begin] = tmp[begin];
		++begin;
	}
}

void mergeSort(vector<int>& v)
{
	int* tmp = new int[v.size()];
	//左闭右闭(要进行归并排序的区间)
	mergeSortChild(v, 0, v.size() - 1, tmp);
	delete[] tmp;
}

非递归代码实现

( 1 ) 递归过程中,每个子区间最终都会只剩下一个元素,再进行二路归并逐层返回.

那n个元素的待排序序列,可以直接看成n组有序序列.

image.png

( 2 ) 两组序列进行二路归并时,要判断是否存在越界行为.

相邻的两组序列:第一组 —— [begin1, end1] ;第二组 —— [begin2, end2]

以下三种情况需要修正:

A end1 >= v.size()

B begin2 >= v.size()

C end2 >= v.size()

void mergeSortNotR(vector<int>& v)
{
	if (v.size() <= 1)
		return;

	int* tmp = new int[v.size()];

	//标识几个元素为1组进行归并
	int group = 1;

	while (group < v.size())
	{
		for (int i = 0; i < v.size(); i += group * 2/*跳过两组数据*/)
		{
			//第一组
			int begin1 = i;
			int end1 = i + group - 1;

			//第二组
			int begin2 = i + group;
			int end2 = i + 2 * group- 1;

			int pos = i;

			//边界修正

			//如果end1越界,修正end1,同时说明第二组不可能存在
			if (end1 >= v.size())
			{
				end1 = v.size() - 1;
				begin2 = v.size();
				end2 = v.size() - 1;
			}
			//begin2越界,第二组数据不存在
			else if (begin2 >= v.size())
			{
				begin2 = v.size();
				end2 = v.size() - 1;
			}
			//只有end2越界,修正第二组数据即可
			else if (end2 >= v.size())
			{
				end2 = v.size() - 1;
			}

			//两组序列进行二路归并
			singleMerge(v, begin1, end1, begin2, end2, pos, tmp);
		}
		//遍历完当前所有的组,所有相邻组 归并后的结果保存在tmp中,结果拷贝回原序列		
		for (int j = 0; j < v.size(); ++j)
			v[j] = tmp[j];

		//更新一组数据的个数
		group *= 2;
	}
	delete[] tmp;
}

时间复杂度

归并排序是严格的二分,所以效率是O(NlogN)

但会消耗O(N)的空间复杂度.

排序的稳定性

稳定性

稳定性是指相同的数,经过排序之后,相对次序保持不变

应用:高考的排名,在总分相同的情况下,要比较语文/数学等成绩确定

例:假设考生的排名只跟总分和语文有关,此时可以先排语文,再排序总分,

image.png

各排序的稳定性

直接插入排序:稳定

希尔排序:不稳定(分组时,相同的数据可能会被分到不同的组里)

直接选择排序:不稳定

image.png

堆排序:不稳定

image.png

冒泡排序:稳定

快速排序:不稳定

image.png

image.png

image.png

归并排序:稳定