分享内容
归并排序算法
虚拟内存空间
用户空间通常包括以下几个部分:
- 代码段(Text Segment):存放程序的指令代码。
- 数据区(Data Segment):存放全局变量和静态变量。
- 栈区(Stack Segment):用于函数调用、参数传递、局部变量存储以及返回地址管理。栈区是动态分配的,但分配方式是自动的,由系统管理。
- 堆区(Heap Segment):用于动态内存分配和释放。堆区的内存分配由程序员通过编程语言提供的内存管理函数(如C语言的malloc/free,C++的new/delete)来控制。
- 预留区(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可用。
划定分数线的过程:当前判断的分数与后面分数相等时,当前分数不可
用,向左选取更高分数作为待选分数线。直到当前判断的分数与后面分
数不相等,当前分数为分数线。

划定分数线(主程序)
主程序流程:
- 输入n、n个成绩和x,成绩保存在数组a
- 对数组a进行降序排序
- 划定分数线
- 输出分数线的值
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];// 结构体数组
临时数组类型与原数组要保存一致

排序规则:
"先按总分排,如果总分相同,则语文分数高者名次考前,如果语文成绩还相同,按输入顺序排"
- 总分大的排前面
cpp
(a [head1] . total > a [head2] . total)
或者两者取其一,即成立。用或运算符连接。
cpp
(a [head1] . total == a[head2].total &&
a [head1] . chinese >= a [head2] . chinese) )
- 总分相等时,语文分数高的排前面
- 总分和语文分都相同时,按输入
顺序排(即相等时左序列优先)

主程序流程:
-
输入n和n个学生的信息到数组a
(需要计算每个学生的总分)
-
对数组a按规则进行归并排序
-
输出前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)的合并过程:





统计逆序对(主程序)
主程序流程:
- 输入n和n个整数
- 对数列进行归并排序并统计逆序对
- 输出逆序对个数
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、面关于排序稳定性的描述,正确的是?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;
}