考点:堆排序、快速排序和归并排序是重难点,可能会考相关的代码;
掌握各种排序的思想、过程和特征(初态的影响、复杂度、稳定性、适用性等),通常考察选择题,同时对于一些特定序列应根据排序算法的特征选择最优排序算法。
目录
一、基本概念
二、插入排序
1.直接插入排序
2.折半插入排序
3.希尔排序(重点,但考代码频率低)
三、交换排序
1.冒泡排序
2.快速排序(内部排序中平均性能最好的算法,很牛)
四、选择排序
1.简单选择排序
2.堆排序(重点!!)
五、归并排序和基数排序
1.归并排序(与堆和快排时间效率相近)
2.基数排序
六、各种内部排序算法的比较及应用
1.算法比较
2.算法应用
七、外部排序
1.概念
2.方法
3.多路平衡和败者树
1.败者树
2.多路平衡+败者树
4.置换-选择算法
5.最佳归并树(本质是哈夫曼树求最短带权路径WPL)
八、课后习题
排序:重新排列表中元素,使表中元素满足关键字有序。
稳定性:排序前表中相同关键字的元素A和B,A在B前,则排序后仍保持A在B前则该算法稳定。
算法评判指标:稳定性、时间复杂度、空间复杂度、(外部排序额外注意)读写磁盘的次数
ps:①注意区分内部排序和内存内排序的概念,如拓扑排序是属于内存内排序的算法,但不属 于内部排序。
②对任意n个关键字排序的比较次数至少为(向上取整)次,证明如下图:
基本思想:将一个待排序序列按关键字大小插入到前面已排序的序列中,直到全部插入完成。
基本思想:每次选取待排序列的第一个元素,将它与已排序子列中元素依次对比,找到合适的位置后移动子列元素,让该元素插入到特定位置,完成排序。
以插入待排元素49为例:
注意直接插入算法在比较时会先让待插入元素与有序子列最后一个元素比较,若大于或等于则直接插入原位置,小于再与子列从头元素开始依次比较。
void InsertSort(int A[], int n)
{
int i, j, temp;
for(i=1; i=0 && A[j]>temp; j--) //查找到位置之后将后面的元素移动腾出空间
{
A[j+1] = A[j];
}
A[j+1] = temp;
}
}
}
算法分析:
空间效率:O(1)
时间效率:最好O(n),最坏O(n^2)——时间复杂度:
最少比较次数为 n-1,最多比较次数为 n(n-1)/2
稳定性:稳定的排序算法
适用性:适用于顺序表和链表(比较次数不变,时间复杂度不变,“移动”是改变指针)
折半插入排序是对直接插入排序的一种优化算法,相对来说在查找方面节省了一定时间。
算法思路:先折半查找到应插入的位置再移动元素。
算法步骤:①利用折半查找前面的有序子表,查找出待插入的元素的应在位置;②为插入元素腾出空间后插入。
void InsertSort(ElemType A[], int n)
{
int i, j, low, high, mid;
for(i = 2; i<=n; i++)
{
A[0] = A[i]; //哨兵式算法
low = 1;
high = i-1;
while(low<=high)
{
mid = (low+high)/2;
if(A[mid]>A[0])
high = mid-1; //查左半子表
else
low = mid+1; //查右半子表
}
for(j = i-1; j>=high+1; j--)
A[j+1] = A[j];
A[high+1] = A[0];
}
}
算法分析:
时间复杂度:,但比较次数降低为
空间复杂度:O(1)
稳定性:稳定
适用性:仅顺序表,链表无法适用
基本思想:将待排表分为若干子表,子表跨度为d(将跨度为d的数据划为一组进行直接插入排序),将子表作为新记录进行直接插入排序,并不断缩小增量d,当整个表中元素“基本有序”(d=1),再对整体进行一次直接插入排序。
以下列数据表为例:
总结步骤:增量最优是一半一半缩小,但考试中可能会遇到各种不同增量。
void ShellSort(int A[], int n)
{
int d, i, j;
//A[0]是暂存单元,并不是哨兵,当j<=0时插入的位置已经到达
for(d=n/2; d>=1; d=d/2)
{
for(i=d+1; i<=n; i++)
{
if(A[i]0 && A[0]
算法分析:
空间复杂度:O(1)
时间复杂度:,且越接近正序比较次数越少
稳定性:不稳定
适用性:顺序表,链表哒咩
交换排序顾名思义,通过比较对调两个元素的记录在序列中的位置完成排序。
基本思想:从后往前两两比较相邻元素的值,若为逆序则交换直到序列比较完成(所需元素冒到前面),每次向前冒一个最大/最小元素,直到全部元素冒完为止,若某一趟排序从未发生过“交换”,则证明此时序列已整体有序。
//交换
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
//冒泡排序————从后往前冒泡
void BubbleSort(int A[], int n)
{
for(int i=0; ii; j--) //从后向前进行“冒泡”
{
if(A[j-1]>A[j])
{
swap(A[j-1], A[j]);
flag = true;
}
}
if(flag == false) //若无发生交换,则说明表已有序,直接退出程序
return;
}
}
//冒泡排序————从前往后冒泡
void BubbleSort(int A[], int n)
{
for(int i=0; iA[j+1])
{
swap(A[j+1], A[j]);
flag = true;
}
}
if(flag == false) //若无发生交换,则说明表已有序,直接退出程序
return;
}
}
算法分析:
空间复杂度:O(1)
时间复杂度:
稳定性:稳定
适用性:顺序表、链表
基本思想:分治法——每次选取一个元素作为基准,将序列分为两部分,递归进行直到每个元素单独成为一个部分后再合并,最终使序列有序。
算法步骤:①选取元素为pivot(基准);②利用pivot与整个序列进行对照,取high和low两个指针,从队首和队尾依次向中间逼近,若high指针发现比pivot更小的元素时停止,当low指针发现比pivot更大的元素时停止,当两个指针都停下后对两个指针所指元素进行交换(好像不完全对)
以下图为例,好好品好好品:
选取49为pivot,设置high和low指针:
先令high指针移动,直到找到比pivot小的元素:
将high所指元素放入low所指空位后,high指向空位,low可开始移动:
当low发现比pivot元素更大的元素时停止,将元素放入high所指空位中:
当low指针空出后停止,high指针有元素继续前移:
依照以上步骤直到low和high相遇,将pivot值放入该区域:
之后再在这个pivot两侧的两个子列中分别进行上述步骤,直到所有子列处理完毕,使整体有序:
具体参考王道PPT,细品细品!!
//用第一个元素对待排序列进行划分
int Partition(int A[], int low, int high)
{
int pivot = A[low];
while(lowpivot)
high--;
A[low] = A[high];
while(low
算法分析:
空间复杂度:O(递归层数) => 最好,最坏O(n)(n-1),平均
时间复杂度:O(n*递归层数)——平均
注意:递归次数与划分后得到的分区处理顺序无关,且每次划分不局限于小的数在前大数在后,需根据题目是升序还是降序来进行选择。
稳定性:不稳定
基本思想:每次在待排序列中选取最大/最小元素,排列到前面已排好子列中
基本思想:每次在待排序列中利用依次对比方法选取最大/最小元素,排列到有序子列中,n个元素的简单选择排序序列需进行n-1趟处理。
void SelectSort(int A[], int n)
{
for(int i=0; i
算法分析:
空间复杂度:O(1)
时间复杂度: 无论是有序乱序还是逆序,都需要进行n-1趟处理
稳定性:不稳定
适用性:顺序表、链表
堆分为大根堆和小根堆,大根堆是指根元素大于全部元素,小根堆是指根元素小于全部元素(这里的根不仅指根节点,也包括子树上面的根)。
堆的建立:(对比完全二叉树的内容学习)
思路:把所有非终端节点检查一遍,若不满足条件(根至少小于/大于左右孩子中的一个),将更大/小的孩子与根节点互换。
注意:在调整时可能上一层根节点的调整会引起下层的调整,所以不是所有节点都绝对只调整一次。
//调整以k为根的子树为大根堆
void HeadAdjust(int A[], int k; int len)
{
A[0] = A[k];
for(int i=2*k; i<=len; i*=2) //查看子树关键字情况
{
//当左右孩子一样大时,优先和左孩子交换,一定程度保证稳定性(堆排序不稳定)
if(i=A[i])
break;
else
{
A[k] = A[i];
k=i; //修改k值,继续向下查找
}
}
A[k] = A[0];
}
//建立大根堆
void BuildMaxHeap(int A[], int len)
{
for(int i=len/2; i>0; i--)
HeadAdjust(A, i, len);
}
//建立小根堆就是把HeadAdjust里面判断条件改为判断更小子树
堆的插入:在队尾插入节点,之后与父节点比对,根据比对结果不断上升。
堆的删除: 被删除元素用堆底元素替代,然后让孩子不断“下坠”至无法下坠
堆排序:每一趟选取堆顶元素加入到有序子列中(与待排序列中最后一个元素交换,之后再调整堆,使其再度成为堆)
基于大根堆的堆排序得到递增序列;基于小根堆的堆排序得到递减序列 (每次堆顶元素与后面元素对调)
根据堆的性质可知,大根堆中最小元素和小根堆最大元素都在叶节点,综合完全二叉树,叶节点编号范围为 m/2(向下取整)+1 ~ n
//建立大根堆
void BuildMaxHeap(int A[], int len);
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len);
//堆排序
void HeapSort(int A[], int len)
{
BuildMaxHeap(A, len);
for(int i=len; i>1; i--)
{
swap(A[i], A[1]); //将堆顶元素与i指针(从后往前)所指元素对换
HeadAdjust(A, 1, i-1);
}
}
算法分析:
空间复杂度:O(1)
时间复杂度:建堆O(n) 排序O(h*n) = (每一趟 = O(h) = ) 总时间复杂度
注意:一个节点每“下坠”一层最多进行两次关键字比对,树高h节点在第 i 层则将节点向下调整最多值下降 h-i 层,关键字比对次数不超过 2(h-i)
稳定性:不稳定
适用性:线性表和链表
基本思路:通过从小到大依次将各个子表进行有序合并生成有序子表,不断归并达到整体有序
注意:当两个待合并子表长度不一样,其中一个表元素已全部合并时,直接将另一子表元素全部加到已合并表后。
m路归并:将m个子表进行归并,选取一个元素时需要进行m-1次比较
以下图为例: (可视为倒立的m叉树,顶部为叶节点)
int *B = (int *)malloc(n*sizeof(int)); //辅助数组,存放已排好的序列
//A[low...mid]和A[mid+1...high]各自有序,将两个部分归并
void Merge(int A[], int low, int mid, int high)
{
int i, j, k;
for(k=low; k<=high; k++)
{
B[k] = A[k];
}
for(i=low, j=mid+1, k=i; i<=mid && j<=high; k++)
{
if(B[i]<=B[j])
A[k] = B[i++];
else
A[k] = B[j++];
}
while(i<=mid)
A[k++] = B[i++];
while(j<=high)
A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high)
{
if(low
算法分析:
空间复杂度:O(n)
时间复杂度: 每趟归并复杂度O(n) ,共需(向上取整)趟 (递归次数)
基数排序不基于比较和移动,而是基于关键字各个位的大小进行排序。
基本思路:设关键字由d个元组构成(...),其中为最主位关键字(权值最高),为 最次位关键字(权值最低);从左到右对关键字进行扫描,在同一权值比对下将相同值的关键字按照权值先后顺序放入到权值对应的队内,最后按权值大小进行整合。
上面都是废话, 还是来看例子吧:
品,你细品~(这个思想不局限于基数排序,其他关键字权值不同的对比也可以用,参考课后题8.4那个选择,好好品品)
//基数排序通常是基于链式存储实现的
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode, *LinkList;
typedef struct
{
LinkNode *front, *rear;
}LinkQueue;
算法分析:
空间复杂度:(r个辅助队列)O(r)
时间复杂度:分配O(n) + 收集O(r) = O(n+r)
稳定性:稳定
注意:基数排序中不要求关键字长度一致,如生日,各个关键字长度不同也可用基数排序
基数排序不仅有LSD(最低位优先)还有MSD(最高位优先)当题目没说是哪种情况时需多方面考虑。
外部排序的提出主要是因为内存空间有限,因此对于较大篇幅的序列进行排序时需借助外存实现,即将待排序的记录存储在外存上,排序时再将数据一部分一部分地调入内存,排序过程中会涉及到多次内外存的交互,因此叫做外部排序。
通常使用归并算法:①根据内存缓冲区大小将外存上文件划分为多个子文件,依次读入内存并利用内部排序的方法对其进行排序,并将得到的有序序列写回外存,这些归并的子文件被成为归并段或顺串;②对这些归并段进行逐趟归并,使归并段有小到大,直到整个文件有序。
总结:以块为单位读入内存进行排序,排好之后再按块写回外存,实际排序还是在内存进行。
注意:对m路平衡归并排序时,若实现输入/内部归并/输出的并行处理,需设置2m个输入缓冲区和2和输出缓冲区;若实现串行处理,则仅需m个输入缓冲区和1个输出缓冲区。
废话不多说直接看例子吧:
构造归并段:依次向两个输入缓冲区读入一块数据,然后利用归并排序从大到小排序,再利用输出缓冲区输出形成一个长度为2块的归并段(注意,每次再写入外存的空间不是原空间,原空间在数据读出后会归还系统,这里只是为了方便观察才画在原位置)
按照上述方法对全部内容进行归并:
进行第二趟归并——将8个归并段归并为4个归并段:
每空出一个缓冲区都要立刻移入对应归并段的一块数据,直到归并段数据全部处理过:
按照上述方法将全部8个归并段进行二次归并:(但其实这一次才叫“第一趟”,所谓“上一次”叫做初始化归并段,不能算做一趟)
以此类推,进行第二趟和狄三趟归并,最终达到整体有序:
优化方法:①实行多路归并(但不是绝对路数越多效率越高,路数多选取一个数据的比较次数也增多,开销同样会增大(但这一点败者树会进行进一步优化))
②减少初始化归并段数量(如上例,直接初始化为第一趟得到的归并段形态,就可以减少一趟归并,置换-选择算法会进行进一步优化)
按照类似足球赛的方式,让元素进行“比赛-晋级”,最终得出“胜者”,可视为一个节点(胜者)带着一个完全二叉树,每个根节点记录这场“比赛”的“胜者”。
基于这种方法,可应用于多路平衡的归并中:
算法分析:
空间复杂度:O(k)(k路归并仅需定义一个长为k的数组)
时间复杂度:O((向上取整))——每次选取胜者的复杂度(原本需要k-1次比较)
置换-选择算法可以在最大程度上减少归并段数量,降低外部排序时间复杂度。
算法思路:利用内存工作区WA对初始待排序的文件FI进行归并段的划分,即每次选取工作区内最小元素进行输出,并记录此时读出数据的值MINIMAX,输出后读取文件中下一块数据(让工作站尽可能长期为满),若该数据读入后,工作区最小元素小于MINIMAX,则忽略该最小元素,继续输出大于MINIMAX的最小元素,直到工作区内无满足输出条件的元素,则以上输出的全部元素被划分为一个初始归并段。
都是废话,上图!!
依次向工作站读入数据,比较输出最小值
当读入后站内最小值大于MINIMAX,忽略该值,输出大于MINIMAX的最小值:
按照上述方法,依次生成全部初始归并段:
在经过置换-选择算法后得到长度不等的初始归并段,对归并段的不同操作方式会引起不同次数的I/O操作,因此引入最佳归并树的概念,即按这种流程对归并段进行处理,引起的I/O操作次数最少:
①以二路归并为例:
注意:当待排序列元素个数不能满足哈夫曼树的生成条件时应增加0值的虚拟元素(不存在)
8.3选择题16~17 考点:对快速排序第二趟后得到的序列的判断
快排的阶段性结果特点:第 i 趟完成时,会有 i 个以上的数出现在它最终要出现的位置,即其左边的数都比他小/大,其右边的数都比他大/小
8.4选择题 3 考点:在多个排列依据时应怎么样选择排列顺序——先从重要度小的开始排列
根据基数排序可知,应先将权值(重要度)低的元素进行排序,即先排k2再排k1,同时为避免印算法不稳定时高权值数据相同时造成错位,因此第二次开始的排序应选择稳定算法(此题中插入排序为稳定算法,简单选择不稳定)。