C++(归并排序算法)

分享内容

归并排序算法

虚拟内存空间

用户空间通常包括以下几个部分:

  1. 代码段(Text Segment):存放程序的指令代码。
  2. 数据区(Data Segment):存放全局变量和静态变量。
  3. 栈区(Stack Segment):用于函数调用、参数传递、局部变量存储以及返回地址管理。栈区是动态分配的,但分配方式是自动的,由系统管理。
  4. 堆区(Heap Segment):用于动态内存分配和释放。堆区的内存分配由程序员通过编程语言提供的内存管理函数(如C语言的malloc/free,C++的new/delete)来控制。
  5. 预留区(Reserved Area):这部分内存通常用于系统扩展或其他特殊用途。

排序算法(回顾)

已经学习过的排序算法:

●冒泡排序通过重复遍历数组,比较并交换相邻元素,使得较大的元素逐渐"冒泡"到

数组未尾,从而实现排序。

● 选择排序进行多次遍历,每轮从未排序区间选择最小的元素,将其放到已排序区间

的末尾。

● 插入排序每轮将未排序区间内的元素插入到已排序区间的正确位置,从而完成排序。

● 快速排序基于分治思想,通过选择一个基准值将数组分为小于和大于基准的两部分,

递归排序这两部分,最终合并为有序数组。

● 计数排序通过统计数据出现的次数来实现排序。

· n是序列长度,w是整数范围

· 稳定性仅针对本课中该算法的实现方式而言

归并排序

● 归并排序是另一个基于分治思想的排序算法

● 归并排序的基本思想是分而治之,将数组分成两部分,分别对这两部分进行排序,然后将排序好的两部分合并成一个有序数组

●简而言之:先拆分,再合并。

归并排序的原理

第一步:合并原则:同层两两合并,同时排序

递归思路

递归实现归并排序:

递归终止条件:序列长度≤1(不可拆分)

突现归并排序

mergeSort完成对数组a指定范围内元素的归并排序

cpp 复制代码
void mergeSort(int left, int right) {
	1、递归终止条件:区间长度为1
	if (left >= right) {
		return;
	}
	2、求出中间下标
	int mid = (left + right) / 2;

	3、对左右序列递归排序
	mergeSort (left, mid);
	mergeSort (mid + 1, right);

4、合并有序序列
	①左右序列首元素比较,合并较小元素到tmp[],直到其中一个序列为空;
	② 把不为空序列的剩余数填充到tmp[];
	③ 将tmp数组赋值给原数组a;
	数组a排序完成!

}

实现归并排序

cpp 复制代码
void mergeSort(int left, int right) {

	4、合并有序序列
	int head1 = left;// 左序列首元素下标
	int head2 = mid + 1;// 右序列首元素下标
	int i = left;//临时数组接收数据的起始位置
	//左右序列首元素比较,合并较小元素到tmp[]
	while (head1 <= mid && head2 <= right) {
		if (a [head1] <= a [head2])
			tmp [i++] = a[head1++];
		else
			tmp [i++] = a[head2++];
	}//注意结束条件为一个序列为空时就停
	// 将剩余元素合并到tmp[]
	while (head1 <= mid)
		tmp [i++] = a[head1++];
	while (head2 <= right)
		tmp[i++] = a[head2++];
	// 将tmp[]中排好序的部分拷贝回原数组中 范围是left~right
	for(i = left; i <= right; i++)
		a[i] = tmp[i];
}
	
cpp 复制代码
void mergeSort (int left, int right) {
	if (left >= right)
		return;
	
	int mid = (left + right)// 2;
	mergeSort (left, mid) ;// 划分左区间
	mergeSort (mid + 1, right); //xa
	
	int headl = left;// 左序列首元素下标
	int head2 = mid + 1;// 右序列首元素下标
	
	int i = left;// 临时数组接收数据的起始位置
	//比较首元素,将较小的合并入辅助数组中
	while (head1 <= mid && head2 <= right) {
		if (a [head1] <= a [head2] )
			tmp [i++] = a[head1++];
		else
			tmp [i++] = a[head2++];
	}
		// 将剩余元素合并到辅助数组中
	while (head1 <= mid)	tmp [i++] = a[head1++];
	while (head2 <= right)	tmp [i++] = a[head2++];
	// 将辅助数组中排好序的部分拷贝回原数组中
	for(i = left; i <= right; i++)
		a[i] = tmp[i];
}

归并排序的性能

● 时间复杂度:拆分产生logn的递归深度,每层合并的总操作数量为n,因此

总体时间复杂度为 O(nlogn)。

● 空间复杂度:递归深度为logn,使用O(logn)大小的栈帧空间。合并操作需

要借助临时数组,使用O(n)大小的额外空间。所以总体空间复杂度为 O(n)。

归并排序的稳定性

划定分数线

宇航局准备招收一批科研人员从事月球探索的航空科研工作。来了n位应聘者,最终选

出不超过x名排名靠前的应聘者。由于可能会有并列分数出现,为了保证公平,同分的

最后几位都不录取,因此可能录取的人数会达不到计划数x。请你编程划定录取分数线,

分数线为最后一名录取的应聘者的分数。(此题要求使用归并排序)

【输入格式】

第一行是一个整数n,代表参加考试的人数(n <= 1000)。

第二行有n个整数,用空格隔开,代表n个人的考试分数。

第三行有一个整数x(x<n),代表宇航局要选出的人数。

【输出格式】

一个整数,代表如果要选出x个人,宇航局应当划出的分数线。

【输入样例】

5

90 75 80 80 95

3

【输出样例】

90

输入:有5个人参加选拔,录取不超过3人。

选人的规则是按照分数高低进行选取,所以先对分数降序排列:

95 90 80 80 75

此题需要划定一个选人的分数线,人要选的尽量多,但是不能超过规定人数。

80分数线:95 90 80 80 75 取4人

90分数线:95 90 80 80 75 取2人

输出:划定分数线为90(录取到2人,符合要求)

划定分数线的过程:

先试图选足3人:

看a3,a3=80。80是否可以作为分数线?

与后一人比较:a4 == a3,80作为分数线会超员,所以80不可用。

试着少选一个人:

看a2:a2=90

与后一人比较:a3 != a2,90作为分数线不会超员,所以90可用。

划定分数线的过程:当前判断的分数与后面分数相等时,当前分数不可

用,向左选取更高分数作为待选分数线。直到当前判断的分数与后面分

数不相等,当前分数为分数线。

划定分数线(主程序)

主程序流程:

  1. 输入n、n个成绩和x,成绩保存在数组a
  2. 对数组a进行降序排序
  3. 划定分数线
  4. 输出分数线的值
cpp 复制代码
//数组长度起码多1

#define MAXN 1010
int a [MAXN], tmp [MAXN] ;

int main() {
	int n,x;
	cin >> n;
	for(int i = 1;i <= n; i++) {
		cin >> ar1
	}
	//数据从下标1开始放
	cin >> x;
	
	// 归并排序
	mergeSort (1, n);//对应的排序范围:1~n
	
	// 找到分数线
	while (a [x] == a [x+1]) {
		x --;
	}
	
	cout << a [x] ;
	return 0;
}

( 完整代码示例)

cpp 复制代码
#include<iostream>
using namespace std;

#define MAXN 1010
int a [MAXN], tmp [MAXN] ;

//mergeSort完成对数组a指定范围内元素的归并排序(降序)
void mergeSort (int left, int right) {
	// 递归终止条件:区间长度 <= 1
	if (left >= right)
		return;
	// 划分数组,每次一分为二
	int mid = (left + right) / 2;
	mergeSort (left, mid) ;// 划分左区间
	mergeSort (mid + 1, right) ; //划分右区间
	
	// 合并有序序列
	int head1 = left;// 左序列首元素下标	
	int head2= mid+1;// 右序列首元素下标
	int i = left;// 临时数组接收数据的起始位置
	
	//将较小的元素放入辅助数组中,注意结束条件为一个序列为空时就停止
	while (head1 <= mid && head2 <= right) {
		if (a [head1] >= a [head2])
			tmp [i++] = a[head1++];
		else
			tmp [i++] = a[head2++];
	}
	//两序列可能不同时为空,将剩余元素合并到辅助数组中
	while (head1 <= mid)
		tmp [i++] = a[headl++] ;
	
	while (head2 <= right)
		tmp [i++] = a[head2++] ;
	
	// 将辅助数组中排好序的部分拷贝回原数组中
	for(i = left; i <= right; i++)
		a[i] = tmp[i];
}	
int main(){
	int n,x;
	cin >> n;
	for(int i = 1;i <= n;i++) {
		cin >> a[i];
	}
	cin >> x;
	
	// 归并排序
	mergeSort(1, n);
	
	// 找到分数线
	while (a [x] == a[x+1]) {
		x --;
	}
	cout << a[x] ;
	
	return 0;
}

奖学金

某小学最近得到了一笔赞助,打算拿出其中一部分为学习成绩优秀的前5名学生发奖学金。

给出n名学生的姓名和语文、数学、英语成绩。现在需要对这些学生的成绩按照规则排序,

决定谁能拿到奖学金。先按总分排,如果总分相同,则语文分数高者名次靠前,如果语文成

绩还相同,按输入顺序排。输出排名前5的学生姓名和总分。

【输入格式】

共n+1行。第1行为一个正整数n <= 300,表示该校参加评选的学生人数。

第2行开始,共n行,每行输入学生的姓名和语文、数学、英语的成绩,空格隔开。

(本题要求用归并排序实现)

【输出格式】

按排名输出5行,依次表示前5名,每行是学生的姓名和总分,空格隔开。

此题需要对成绩进行降序排列,排序时名字要和成绩一起动。可以定义结构体保存。

√姓名和总分需要输出,保存

√语文分数和总分影响排序过程,保存

· 数学和英语成绩不影响排序(可以不用保存)

cpp 复制代码
struct Student {
	string name;// 姓名
	int total, chinese;//总分,语文分
};

根据问题描述,学生数量n <= 300,声明学生数组和归并排序需要用到的临时数组:

cpp 复制代码
Student a[300], tmp[300];// 结构体数组

临时数组类型与原数组要保存一致

排序规则:

"先按总分排,如果总分相同,则语文分数高者名次考前,如果语文成绩还相同,按输入顺序排"

  1. 总分大的排前面
cpp 复制代码
(a [head1] . total > a [head2] . total)

或者两者取其一,即成立。用或运算符连接。

cpp 复制代码
(a [head1] . total == a[head2].total &&
a [head1] . chinese >= a [head2] . chinese) )
  1. 总分相等时,语文分数高的排前面
  2. 总分和语文分都相同时,按输入
    顺序排(即相等时左序列优先)
主程序流程:
  1. 输入n和n个学生的信息到数组a

    (需要计算每个学生的总分)

  2. 对数组a按规则进行归并排序

  3. 输出前5名学生的姓名和总分

cpp 复制代码
int main() {
	int n;
	int math, english;
	cin >> n;
	for(int i = 0;i < n;i++) {
		cin>>a [i] . name>>a [i] .chinese>>math>>english;
		a [i] . total = a[i].chinese + math + english;
	}
	//数据从下标0开始放
	
	// 归并排序
	mergeSort(0, n-1);//对应的排序范围:0~n-1
	
	// 输出前5人信息
	for(int i=0;i<5;i++)
		cout << a[i].name << " " << a[i].total << endl;
	return 0;
}
完整代码示例
cpp 复制代码
#include <iostream>
using namespace std;

// 定义结构体
struct Student{
string name; // #t%
int total,chinese;//总分,语文分

Student a[300],tmp[300];//结构体数组

//mergeSort完成对数组a指定范围内元素的归并排序(降序)
void mergeSort (int left, int right) {
	//递归终止条件:区间长度 <= 1
	if (left >= right)
		return;
	
	//划分数组,每次一分为二
	int mid = (left + right) / 2;
	mergeSort (left, mid);// 划分左区间
	mergeSort (mid + 1, right); // 划分右区间
	// 合并有序序列
	int headl = left;// 左序列首元素下标
	int head2=mid+1;// 右序列首元素下标
	int i = left;//临时数组接收数据的起始位置
	
	//比较首元素进行归并,注意结束条件为一个序列为空时就停止
	while (headl <= mid && head2 <= right) {
	// 先按总分排,如果总分相同,则语文分数高者名次考前,如果语文成绩还相同,按输入顺序排
	if (a[head1] . total > a[head2].total
		|| (a[head1] . total == a[head2]. total && a[head1] .chinese >= a[head2] .chinese) )
	tmp [i++] = a[head1++];
	else
		tmp [i++] = a[head2++];
	}
	//两序列可能不同时为空,将剩余元素合并到辅助数组中
	while (head1 <= mid)
		tmp[i++] = a[head1++];
	
	while (head2 <= right)
		tmp[i++] = a[head2++];
	
	//将辅助数组中排好序的部分拷贝回原数组中
	for(i = left; i <= right; i++)
		a[i] = tmp[i];
}
int main() {
	int n;
	int math, english;
	cin >> n;
	for(int i=0;i<n;i++) {
		cin >> a[i].name >> a[i].chinese >> math >> english;
		a[i]. total = a[i].chinese + math + english;
	}
	mergeSort(0, n-1);
	
	for(int i=0;i<5;i++)
		cout << a[i].name << " " << a[i].total << endl;
	return 0;
}	
	
	
	
	

统计逆序对

逆序对是指在一个序列中,如果存在两个元素满足前面的元素大于后面的元素,且前面

的元素在序列中的位置在后面的元素之前,那么这两个元素就构成一个逆序对。具体来

说,对于序列中的元素ai和aj,如果满足i<j且ai>aj,则(ai,aj)就是一个逆序对。

请求出整数序列A的所有逆序对个数。

【输入格式】共2行。第一行输入一个整数n(0<n<1000)。第二行输入n个整数。

【输出格式】一个整数表示逆序对个数。

【输入样例】6

9758310

【输出样例】8

在数字序列中,两个数字一组,当这组数字左侧数>右侧数时,可以称为逆序对。

1、最原始的方法,利用两重循环讲行枚举。

2、利用归并排序的思想求解逆序对的个数,这是解决该问题的一种较为高效的算法

O(nlogn)

先看左序列(9、7、5)的合并过程:

统计逆序对(主程序)

主程序流程:

  1. 输入n和n个整数
  2. 对数列进行归并排序并统计逆序对
  3. 输出逆序对个数
cpp 复制代码
int main() {
	int n;
	cin >> n;
	for(int i = 0;i < n; i++) {
	cin >> a[i] ;
	
	}
	
	// 通过归并排序统计逆序对个数
	mergeSort(0, n-1);
	
	// 输出统计结果
	cout << count;
	return 0;

}

完整代码

cpp 复制代码
#include<iostream>
using namespace std;

#define MAXN 1000
int a [MAXN], tmp [MAXN] ;
int count;// 逆序对个数

//mergeSort完成对数组a指定范围内元素的归并排序(升序)
void mergeSort (int left, int right) {
	// 递归终止条件:区间长度 <= 1
	if (left >= right)
		return;
	
	//划分数组,每次一分为二
	int mid = (left + right) / 2;
	mergeSort (left, mid);// 划分左区间
	mergeSort(mid+ 1, right);// 划分右区间
	
	// 合并有序序列
	int headl = left;// 左序列首元素下标
	int head2 = mid+1;//右序列首元素下标
	int i = left;// 临时数组接收数据的起始位置
	
	//将较小的元素放入辅助数组中,注意结束条件为一个序列为空时就停止
	while (head1 <= mid && head2 <= right) {
		if (a[head1] <= a[head2])
			tmp [i++] = a[head1++];
		else{
			tmp [i++] = a[head2++];
			count += mid -head1 +1;//统计逆序对
		}
	}
	
	// 两序列可能不同时为空,将剩余元素合并到辅助数组中
	while (head1 <= mid)
		tmp [i++] = a[head1++] ;
	
	while (head2 <= right)
		tmp [i++] = a[head2++];
	
	//将辅助数组中排好序的部分拷贝回原数组中
	for(i = left; i <= right; i++)
		a[i] = tmp[i];
}
int main () {
	int n;
	cin >> n;
	for(int i=0;i<n;i++) {
		cin >> a[i];
	}
	// 通过归并排序统计逆序对个数
	mergeSort( 0, n - 1);
	// 输出
	cout << count;
	
	return 0;
}

本次课程的知识点

  1. 归并排序的原理和方法
  2. 归并排序的应用:划定分数线、奖学金、统计逆序对
    1、面关于排序稳定性的描述,正确的是?B

A、稳定性指算法的时间复杂度恒定

B、稳定排序保证序列中相同值的相对顺序不变

C、选择排序是稳定排序

D、插入排序不是稳定排序

2、以下关于归并排序的描述,哪一项是错误的?C

A. 归并排序是一种分治算法

B. 归并排序的时间复杂度为O(nlogn)

C.归并排序是一种不稳定的排序算法

D.归并排序需要额外的存储空间

第k小的数

给定一个长度为n(0<n <= 10000)的序列,保证每一个序列中的数字ai是正整数,编程求出整个序列中第k小的数字。(本题要求用归并排序实现)

【输入】第一行为2个数n,k(0<k <= n)第二行为n个数,表示这个序列。

【输出】第k小的数

【输入样例】5 2

1 2 3 4 5

【输出样例】2

cpp 复制代码
#include<iostream>
using namespace std;
#define MAXN 10010
int a [MAXN], tmp [MAXN] ;

// mergeSort完成对数组a指定范围内元素的归并排序(升序)
void mergesort (int left, int right) {
	// 递归终止条件:区间长度 <= 1
	if (left >= right)
		return;
	// 划分数组,每次一分为二
	int mid = (left + right) / 2;
	mergeSort (left, mid);// 划分左区间
	mergeSort (mid + 1, right) ; //划分右区间
	// 合并有序序列
	int headl = left;//左序列首元素下标
	int head2=mid +1;//右序列首元素下标
	int i = left;//临时数组接收数据的起始位置
	//将较小的元素放入辅助数组中,注意结束条件为一个序列为空时就停止
	while (head1 <= mid && head2 <= right) {
		if (a[head1] <= a[head2])
			tmp[i++] = a[headl++];
		else
			tmp[i++] = a[head2++] ;
	}
	//两序列可能不同时为空,将剩余元素合并到辅助数组中
	while (head1 <= mid)	tmp[i++] = a[head1++];
	while (head2 <= right)	tmp [i++] = a[head2++];
	//将辅助数组中排好序的部分拷贝回原数组中
	for(i = left; i <= right; i++)	a[i] = tmp[i];
}
	
int main () {
	int n,k;
	cin >> n >> k;
	for(int i=1;i <= n;i++) {
		cin >> a[i];
	}
	mergeSort(1,n);//归并排序
	cout << a[k] << "";//输出第k个元素
	return 0;
}