文章目录
一、归并排序
归并是将两个或两个以上的有序表合并为一个新的有序表的过程。
1.归并排序
在合并两个有序表的过程中,不断比较当前两个表中待操作元素,将其中较小(大)的放入新表,使得得到的新表也为有序。
二路归并排序:
void Merge(int left,int right,int mid){
int i=left,j=mid+1;k=left;
while(i<=mid && j<=right){
if(SeqList.Key[i]<=SeqList.key[j]){
MergedList.Key[k] = SeqList.Key[i];i++;k++;
}
else{
MergedList.Key[k] = SeqList.Key[j];j++;k++;
}
}
while(i<=mid){
MergedList.Key[k] = SeqList.Key[i];
j++;
k++; //插入当前剩余的元素
}
while(j<=right){
MergedList.Key[k] = SeqList.Key[j];
j++;
k++;2
}
}
复杂度分析:
设待归并的两个有序表长度分别为 m m m与 n n n,则二路归并后合成的表长度为 m + n m+n m+n;二路归并操作至多需要 m + n m+n m+n次比较和 m + n m+n m+n次移位,故二路归并的时间复杂度为 o ( m + n ) 。 o(m+n)。 o(m+n)。
2. 2-路归并排序
- 将n个记录看成是n个有序序列;
- 将前后两两相邻的两个有序序列归并为一个有序序列(二路归并);
- 重复二路归并操作,知道只有一个有序序列为止。
实现(伪代码):
List SeqList,MergedList;
void MergeSort(){
int left,mid,right,step,n;
n = SeqList.len;
for(step=1;step<n;step*=2){
for(left=1;left<=n;left+=2*step){ //选择前后两组,每组的长度为step,故每次递增左界left的值为2*step
mid = left + step -1;
if(mid>n) break; //若此时越界则跳出循环
right = left + 2*step -1;
if(right>n) right=n; //将整个数组的长度作为右界
Merge(left,mid,rigt); //调用归并排序算法
}
for(int i=1;i<=n;i++) SeqList.Key[i]=MergedList.Key[i]; //更新序列的值
}
}
复杂度分析:
若待排序记录为 n n n个,则需要做 l o g 2 n log_2n log2n趟二路归并排序,每趟二路归并排序的时间复杂度为 o ( n ) 。 o(n)。 o(n)。,因此2-路归并排序的时间复杂度为 o ( n l o g 2 n ) o(nlog_2n) o(nlog2n)。归并排序是一种稳定的排序方法。
二、基数排序
有时候排序不只有一个关键字,可能包含多个;而基数排序是按待排序的关键字组成成分(或“位”)进行排序。
基数排序不需要进行关键字的比较和记录的移动,而是借助于多关键字排序思想实现单逻辑关键字的排序。
多关键字排序的基本思想:
设有n个待排序记录,每个记录是由确定的
d
d
d(部分)位组成,每位有确定的
r
e
d
i
x
redix
redix种取值,则按照关键字的不同值将记录“分配”到
r
a
d
i
x
radix
radix各队列中后再“收集”,如此重复
d
d
d次,可以得到记录的有序序列,其间无需进行关键字间的比较。
1.最高位优先(MSD,Most Significant Digit first)
即先排序最重要的关键字(位)。
-
从最高位关键字 K 1 K^1 K1起进行排序,将记录序列分为若干子序列,每个子序列有相同的 K 1 K^1 K1值;
-
分别对每个子序列再按 K 2 K^2 K2排序,每个子序列又被分为若干个更小的子序列;
-
如此重复,直到将最后一个关键字 K d K^d Kd进行排序;
-
最后,加个所有子序列依次联接成一个有序的序列。
2.最低位优先(LSD,Least Significant Digit first)
思想与上述MSD一致,不过是从最低位关键字 K d K^d Kd排起。
LSD举例:
3.链式基数排序
以链表的结构实现基数排序,可以采取LSD或MSD。
-
首先以链表存储 n n n个待排序记录。
-
一趟排序的过程:
• 分配:按 K i K^i Ki值的升序排序,改变记录指针,将链表中的记录结点依次分配到 r a d i x radix radix个链表队列中,每个队列中所有记录的对应关键字位 K i K^i Ki都相等,用
f[i]
,e[i]
作为第 i i i个链表队列的头指针和尾指针;• 收集:改变所有非空链队列的对位记录的指针域,使其指向下一个非空链队列的队头记录,从而将 r r r个链队列中的记录重新连接成一个链表;
-
如此依次再按关键字的重要级次序分别进行,共进行 d d d趟后即完成排序。
三、插入排序
1.直接插入排序
- 不断将第 i i i个元素,插入到前面已排序的 r [ 1 ] , r [ 2 ] , … , r [ i − 1 ] r[1],r[2],…,r[i-1] r[1],r[2],…,r[i−1]中;
- 用 r [ i ] r[i] r[i]的关键字与 r [ i − 1 ] , r [ i − 2 ] , … r[i-1],r[i-2],… r[i−1],r[i−2],…的关键字顺序进行比较,若小于(大于) r [ x ] r[x] r[x],则将 r [ x ] r[x] r[x]及之后的元素向后移动(插入位置后的元素向后顺移);
- 将 r [ i ] r[i] r[i]插入。
代码实现:
#include<iostream>
using namespace std;
int main(){
int num;
cin>>num;
int *array = new int[num];
for(int i=0;i<num;i++) cin>>array[i]; //列表初始化
for(int i=1;i<num;i++){
int temp;
for(int j=0;j<i;j++){ //由第0个元素开始,向后循环
if(array[i]<=array[j]){
temp = array[i];
for(int k=i;k>j;k--){ //向后移动r[x]及之后的元素
array[k]=array[k-1];
}
array[j] = temp; //插入r[i]
break;
}
}
for(int j=0;j<num-1;j++){
cout<<array[j]<<" ";
}
cout<<array[num-1]<<endl;
}
delete []array;
}
2.希尔排序
由直接插入排序可以看出,当待排序序列基本有序时,插入排序效率会提高;而希尔排序是先将待排序序列分为若干子序列,分别进行插入排序,待整个序列基本有序时,再对全体记录进行一次直接插入排序。于是,希尔排序又称为缩小增量排序。
算法:
- 取一个整数
gap
<n(待排序记录数)作为间隔,将全部记录分为gap
个子序列,所有距离间隔为gap
的记录为同一个子序列; - 在每个子序列列分别施行直接插入排序;
- 缩小间隔
gap
; - 重复上述子序列的划分与排序,直到最后
gap=1
,即对整个列表进行插入排序为止。
实现:
#include<iostream>
using namespace std;
class List{
private:
int *list;
int len;
public:
List();
~List();
void shellSort();
void insertSort(int gap,int m);
};
List::List(){
cin>>len;
list = new int[len];
for(int i=0;i<len;i++){
cin>>list[i];
}
}
List::~List(){
delete []list;
}
void List::shellSort(){
for(int gap=len/2;gap>=1;gap/=2){
for(int m=0;m<gap;m++){ //内循环迭代不同的子序列
insertSort(gap,m);
}
for(int j=0;j<len;j++){ //输出每一趟排序的结果
cout<<list[j]<<" ";
}
cout<<endl;
}
}
void List::insertSort(int gap,int m){ //插入排序
for(int i=m;i<len;i+=gap){
int temp;
for(int j=m;j<i;j+=gap){
if(list[j]<list[i]){
temp = list[i];
for(int k=i;k>j;k-=gap){
list[k] = list[k-gap];
}
list[j] = temp;
break;
}
}
}
}
四、快速排序
任取待排序记录中某个记录(例如第一个记录)为基准(枢),将记录序列划分为左右两个子序列。左侧子序列中的所有记录都小于或大于基准记录,右侧子序列中的所有记录都大于或等于基准记录的。然后递归地对两个子序列调用快速排序算法。
基准记录也称 枢纽(或支点) 记录。
算法:
-
取序列第一个记录为枢纽记录,其关键字为
Pivotkey
; -
指针
low
指向序列第一个记录位置,指针high
指向序列最后一个记录位置; -
对序列进行一趟排序:
-
从
high
指向的记录开始,向前找到第一个小于Pivotkey
的记录,将其放到low
指向的位置,low
++(由于Pivotkey
保存了此时low
指向的值,故不用担心数据覆盖); -
从
low
指向的记录开始,向后找到第一个大于Pivotkey
的记录,将其放到high
指向的位置,high
–(此时high
的值已被换到上一步的low
中了,故也不用担心数据被覆盖); -
重复1,2,直到
low=high
,将枢纽记录放在low(high)
指向的位置。
-
实现:
void qsort(int low,int high){
int i=low,j=high;
int pivot = list[low];
while(low<high){
while(low<high&&pivot<=list[high]) high--;
if(low<high){
list[low]=list[high];
low++;
}
while(low<high&&pivot>=list[low]) low++;
if(low<high){
list[high]=list[low];
high--;
}
}
list[low]=pivot;
for(int i=0;i<len-1;i++){
cout<<list[i]<<" ";
}
cout<<list[len-1]<<endl;
if(i<low-1) qsort(i,low-1);
if(j>high+1) qsort(high+1,j);
}
五、堆排序
堆在逻辑意义上为一棵二叉树
- 定义:有一个关键字集合,按完全二叉树的顺序存储方式存放于一个一维数组中。若其中的每个节点的关键字都大于(或小于)其子节点的关键字,则称该关键字集合构成一个最大堆(最小堆)。
或称大顶堆/小顶堆。
而使用堆排序主要要解决两个问题:
- 如何根据给定序列创建初始堆
- 如何在交换掉根节点后,将剩下的节点调整为新的堆(筛选)。
首先看一次筛选:
1. 筛选
- 比较根节点于两个子节点的值,若小于其中一个子节点,则选择最大的子节点与根节点交换
- 继续将交换的节点与其子节点比较;
- 直到叶子节点或根节点值大于两个子节点;
筛选操作保证了当前堆为最大堆,即根节点的值为整个堆的最小值。
2. 创建初始堆
- 根据给定序列,从1至n按顺序创建完全二叉树;
- 由最后一个非终端节点(==第n/2个节点)开始至第一个节点,逐步做筛选。
3. 堆排序
- 创建初始堆,设置已排序序列与未排序序列;
- 由于最大堆的根节点为整个未排序序列的最大值,故将根节点与未排序序列的最后一个节点交换,未排序序列-1,已排序序列+1;
- 对交换完的堆进行从根节点开始的筛选;
- 重复以上2~3。
若要实现升序排序,则创建大顶堆;若实现降序排序,则创建小顶堆。
堆排序举例:
实现:
由以上介绍可知,实现堆排序最重要的步骤即为定义筛选函数,该函数能递归地对当前节点及其子节点进行操作。
select(int n){
int temp,min = list[n];
int loc=n;
if(((2*n+1)<ulen)&&(list[2*n+1]<min)){
min = list[2*n+1];
loc = 2*n+1;
}
if(((2*n+2)<ulen)&&(list[2*n+2]<min)){
min = list[2*n+2];
loc = 2*n+2;
}
temp = list[n];
list[n]=min;
list[loc]=temp;
if(loc!=n){
select(loc);
}
}