快速排序
-
概念:
- 算法思想:在待排序表L[1...n]中任取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1...k-1]和L[k+1...n],使得L[1...k-1]中所有元素小于pivot,L[k+1...n]中所有元素大于等于pivot,则pivot放在了其最终位置L[k]上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分只有一个元素或空为止,即所有元素放在了其最终位置上。
-
代码:
//快速排序
//用第一个元素将排序序列划分为左右两个部分
int Partition(int A[], int low, int high){
int pivot = A[low]; //第一个元素作为枢轴
while(low<high){//用low,high搜索枢轴的最终位置
while(low<high && A[high]>=pivot){
--high;
}
A[low]=A[high]; //比枢轴小的元素移动到左端
while(low<high && A[low]<=pivot){
++low;
}
A[high]=A[low]; //比枢轴大的元素移动到右端
}
A[low] = pivot; //枢轴元素存放到最终位置
return low; //返回存放枢轴的最终位置
}
//快速排序
void QuickSort(int A[], int low, int high){
if(low < high){//递归跳出的条件
int pivotpos = Partition(A,low,high);//划分
QuickSort(A,low,pivotpos-1);//划分左子表
QuickSort(A,pivotpos+1,high);//划分右子表
}
}
-
tip1:
- 若每一次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高
- 快速排序算法优化思路:尽量选择可以把数据中分的枢轴元素
- eg:
- 1)头、中、尾三个位置的元素,取中间值作为枢轴元素。
- 2)随机选一个元素作为枢轴元素
-
tip2:
- 若每一次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低
- 若初始序列有序或逆序,则快速排序的性能最差,因为每次选择的都是最靠边的元素
-
算法效率分析
- 总结
选择排序
-
概念:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
-
分类:简单选择排序、堆排序
简单选择排序
- 代码:
//简单选择排序
void SelectSort(int A[], int n){
for(int i = 0; i < n; i++){ //一共进行n-1趟
int min = i; //记录最小元素位置
for(int j = i+1; j < n; j++){ //在A[i...n-1]中选择最小的元素
if(A[j] < A[min]){
min = j; //更新最小元素位置
}
}
if(min != i) swap(A[i],A[min]); //交换数据
}
}
// 交换,共移动元素3次
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
- 总结
堆排序
- 概念
- 若n个关键字序列L[1...n],满足下面某一条性质,则称为堆 (Heap):
- 1)若满足:L(i)>=L(2i)且L(i)>=L(2i+1) (1<=i<=n/2)————大根堆(大顶堆)
- 2)若满足:L(i)<=L(2i)且L(i)<=L(2i+1) (1<=i<=n/2)————小根堆(小顶堆)
- 二叉树的顺序存储
- 建立大根堆:
- 思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
- 1)检查当前结点是否满足 跟>=左、右
- 2)若不满足,将当前结点与更大的一个孩子互换
- 3)若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)
- 建立大根堆代码:
//建立大根堆
void BuildMaxHeap(int A[], int len){
for(int i = len/2; i > 0; i--){
//从后往前调整所有非终端结点
HeadAdjust(A,i,len);
}
}
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){
A[0] = A[k]; //A[0]暂存子树的根结点
for(int i=2*k; i<=len; i*=2){//沿key较大的子结点向下筛选
if(i<len && A[i] < A[i+1])
i++; //取key较大的子结点的下标
if(A[0]>=A[i]) break; //筛选结束
else{
A[k] = A[i]; //将A[i]调整到双亲结点上
k = i; //修改k值,以便继续向下筛选
}
}
A[k] = A[0]; //被筛选结点的值放入最终位置
}
- 堆排序:每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换),并将待排序元素序列再次调整为大根堆(小元素不断下坠)
- 注意: n-1趟处理之后
- 基于大根堆的堆排序得到“递增序列”
- 基于小根堆的堆排序得到“递减序列”
- 代码:
//建立大根堆
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--){ //n-1趟的交换和建堆过程
swap(A[i], A[1]); //堆顶元素和堆底元素交换
HeadAdjust(A, 1, i-1); //把剩余的堆排序元素整理成堆
}
}
-
结论:
- 一个结点,每下坠一层,最多只需对比关键字2次
- 若树高为h,某结点在第i层,则将这个结点向下调整,最多只需下坠h-i层,关键字对比次数不超过2(h-i)
- 建堆的过程,关键字对比次数不超过4n,建堆时间复杂度=O(n)
- 根节点最多下坠h-1层,
- 每下坠一层,最多只需对比关键字2次,因此每一趟排序复杂度不超过O(h)=O(log2n)
-
总结:
大/小根堆的插入/删除操作
插入元素:
- eg: 对于小根堆:新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止
删除元素:
-
eg: 对于小根堆:被删除的元素用堆底元素替代,然后让该元素不断下坠,直到无法下坠为止
-
关键字对比次数:
- 每次“上升”调整只需对比关键字1次
- 每次“下坠”调整可能需要对比关键字2次,或者1次