目录
2.1 直接插入排序(Straight Insertion Sort)
1 基本定义
内排序:整个排序过程在内存中完成
排序的稳定性
- 存在多个具有相同排序码的记录
- 排序后这些记录的相对次序保持不变
排序算法的时间代价:记录的比较和移动次数
2 插入排序
2.1 直接插入排序(Straight Insertion Sort)
发扑克牌的时候我喜欢一边摸牌,一边理牌。这个过程就类似于直接插入排序。
基本操作:将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数+1的有序表。
2.1.1 栗子
本来有牌45
抽到了一张34,放45的前面
抽到了一张78,放45的后面
抽到了一张12,依次跟78,45,34比较,最后放在45的前面
其他同理
2.1.2 代码实现
template<class Record>
void ImprovedInsertSort(Record Array[], int n){//Array为待排数组,n为数组长度
Record TempRecord; //临时变量
//将待排元素一个个取出,存到临时变量里
for(int i=1; i<n; i++){
TempRecord = Array[i];
//记录i往前与序列中的元素比较
int j = i - 1;
//将大于等于记录i的记录后移(j=-1时,即原序列无元素,记录i为第一个元素)
while(j >= 0 && (TempTecord < Array[j])){
Array[j+1] = Array[j];
j = j - 1;
}
//此时j后面就是记录i的位置
Array[j+1] = TempRecord
}
}
2.1.3 算法分析
2.2 Shell排序
基于直接插入排序的两个性质
- 序列本身有序的情况下,时间代价为O(n)
- 对于短排序,直接插入排序比较有效
Shell排序算法思想
- 将序列转化为若干小序列,在这些小序列内进行插入排序
- 逐渐壮大小序列的规模,减少序列个数,使待排序序列逐渐处于更有序的状态
- 最后对全体记录进行一次直接插入排序
2.2.1 栗子——增量每次除以2递减
选择增量为4的两个元素为一组(4 = 待排序列总数 / 2)
采用直接插入排序
选择增量为2的4个元素为一组,采用直接插入排序
最后一轮,增量为1,对全部记录进行一次直接插入排序
2.2.2 代码实现
template<class Record>
void ShellSort(Record Array[], int n){//Array[]为待排序数组,n为数组长度
int i, delta;
//增量delta每次除以2递减,直到delta=1
for(delta = n/2; delta>0; delta /=2)
for(i = 0; i<delta; i++)
//分别对delta个子序列进行插入排序
//&传Array[i]的地址,数组总长度为n-i
//i=0,数组长为n,delta=n/2
ModInsSort(&Array[i],n-i,delta);
//如果增量序列不能保证最后一个delta间隔为1
//可以安排下面这个扫尾性质的插入排序
//ModInsSort(Array, n, 1)
}
针对增量而修改的插入排序算法
template<class Record>
void ModInsSort(Record Array[], int n, int delta){
int i, j;
//对子序列中第i个记录,寻找合适的插入位置
//以下标为delta的记录开始处理,其前面的元素下标为0
for(i = delta; i < n; i += delta)
//j以delta为步长向前寻找逆序对进行调整
for(j = i; j >= delta; j -= delta){
if(Array[j] < Array[j-delta])//逆序对
swap(Array,j,j-delta);//交换
else break;
}
}
2.2.3 算法分析
2.2.4 增量选择
(1)增量不互质
选取的增量之间不互质,导致重复排序。如
45与34'第一轮在同一子序列里
第二轮还在同一子序列里
(2)增量互质
3 选择排序
3.1 直接选择排序
依次选出剩下的未排序的记录中的最小记录。
过程类似于打擂台。
3.1.1 栗子
第一回合
第一个站在擂台上的是45,34去跟45打,34赢了。
78去跟34打,78输了,目前站在擂台的还是34。
12去跟34打,精彩,34输了,12领先。
34'去跟12打,输了。32去跟12打,输了。29去跟12打,输了。64去跟12打,还是输。
第一回合,获胜的是12!其他同理。
3.1.2 代码实现
template<class Record>
void SelectSort(Record Array[], int n){
//依次选出第i小的记录,即剩余记录中最小的那个
for(int i=0; i<n-1; i++){
//首先假设记录i就是最小的
int smallest = i;
//开始向后扫描所有剩余记录
for(int j=i+1; j<n; j++)
//如果发现更小的记录,记录它的位置
if(Array[j] < Array[smallest])
smallest = i;
swap(Array,i,smallest);//将smallest赋值给Array[i]
}
}
3.1.3 算法分析
3.2 堆排序
基于最大堆来实现。而堆的本质是一棵用数组表示的完全二叉树。
3.2.1 栗子
(1)建立最大堆
将已经存在的N个元素按最大堆的要求存放在一个一维数组中。
(2)找到最大值就将其删除。即将堆里的最后元素与堆顶元素交换
(3)最大值78出堆,堆的规模-1。剩余的元素从根开始调整。
3.2.2代码实现
template<class Record>
void sort(Record Array[], int n){
int i;
//建堆
MaxHeap<Record> max_heap = MaxHeap<Record>(Array,n,n);
//算法操作n-1次,最小元素不需要出堆
for(i=0; i<n-1; i++)
//依次找出剩余记录中的最大记录,即堆顶
max_heap.RemoveMax();
}
3.2.3 算法分析
此外可以改写堆排序算法,发现逆序对直接交换。
4 交换排序
4.1 冒泡排序
不断比较相邻的记录,不满足排序要求的就交换相邻记录,直到所有记录都已经排好序。
4.1.1 栗子
从最后开始。发现逆序对就交换。
发现逆序对32 29
交换位置
发现逆序对34’ 29
交换位置
......第一轮比较结束后,选出了最小元素12
直接选择排序中没有这么多次的交换,站在擂台上的数不一定是最小数。
4.1.2 代码实现
template<class Record>
void BubbleSort(Record Array[], int n){
bool NoSwap; //用来标识是否发生了交换
int i, j;
for(i = 0; i<n-1; i++){//比较n-1轮
NoSwap = true; //标识初始为true
for(j = n-1; j>i;j--){
if(Array[j] < Array[j-1]){//判断是否为逆序对
swap(Array,j,j-1);//交换逆序对
NoSwap = false; //发生了交换,标志为false
}
}
if(NoSwap) //这一轮的元素都是排好序的,没发生交换,直接退出冒泡排序
return;
}
}
4.1.3 算法实现
4.2 快速排序
- 选择轴值pivot(核心)
- 将序列划分成两个子序列L和R,使得L中所有记录都小于或等于pivot,R中记录都大于pivot
- 对子序列L和R递归进行快速排序
- 基于分治策略
4.2.1 轴值的选择(核心)
尽可能使得L,R长度相等
- 选择首元素或尾元素作为轴值(完全正序或完全逆序的输入就会比较糟糕)
- 随机选择
- 选择平均值(代价)
4.2.2 栗子
一次的分割过程
选择32作为轴值
然后把最右端的元素64填充进去,然后最右边的位置就空出来了
接着从左边开始看,将左边的元素25与轴值32做比较。
25<32,不是逆序元素,i继续往右走指向34,34>32,34填充到最右边的位置。此时左边空出了一个位置。
此时左边空出了一个位置。移动右边的j,从右边找一个逆序元素29填入左边的空位。
此时右边空出了一个位置。从左边找一个逆序元素45填入右边的空位。
风水轮流转,左边有空位了。继续移动右边的j,从右边找一个逆序元素12填入左边的空位
最终
具体过程描述:
- 选择轴值并存储轴值(上述例子取中间记录作为轴值)
- 最后一个元素放到轴值位置
- 初始化下标 i, j,分别指向头尾
- 递增直到遇到比轴值大的元素,将此元素覆盖到 j 的位置;j 递减直到遇到比轴值小的元素,将此元素覆盖到 i 的位置
- 重复上一步直到 i==j,将轴值放到 i 的位置,完毕
4.2.3 代码实现
template<class Record>
void QuickSort(Record Array[], int left, int right){
//Array[]为待排序数组,left,right分别为数组两端
if(right <= left) //如果只有0个或1个记录,就不需要排序
return;
int pivot = SelectPivot(left,right); //选择轴值
swap(Array,pivot,right); //轴值放到数组末端
pivot = Partition(Array,left,right); //分割后轴值正确
QuickSort(Array,left,pivot-1); //左子序列递归快排
QuickSort(Array,pivot+1,right); //右子序列递归快排
}
int SelectPivot(int left,int right){
//选择轴值,参数left,right分别表示序列的左右端下标
return(left+right)/2; //选中间记录作为轴值
}
分割函数
template<class Record>
int Partition(Record Array[],int left,int right){
//分割函数,分割后轴值已到达正确位置
int l = left; //l为左指针
int r = right; //r为右指针
Record TempRecord = Array[r];//保存轴值
while(l != r){//l,r不断向中间移动,直到相遇
//l指针向右移动,直到找到一个大于轴值的记录
while(Array[l] <= TempRecord && r>l)
l++;
if(l<r)//未相遇,将逆置元素换到右边空位
Array[r] = Array[l];
r--; //r指针向左移动一步
//r指针向左移动,直到找到一个小于轴值的记录
while(Array[r] >= TempRecord && r>l){
r--;
if(l < r){ //未相遇,将逆置元素换到左空位
Array[l] = Array[r];
l++; //l指针向右移动一步
}
}//end while
Array[l] = TempRecord;//将轴值回填到分界位置上
return l; //返回分界位置l
}
4.2.4 算法分析
你们以为这就结束了?天真!
学习自:
张铭《数据结构》