快速排序
快速排序是一种分治排序算法,采用了递归的方法。
原理:
1.先从数列中取出一个数作为基准数。
2.分区过程:将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.对左右区间重复第二步,直到各区间只有一个数。
代码实现为:
int partition(Item a[],int l,int r) {
...
}
void quick(Item a[],int l,int r) {
if (l>=r) return;//如果子区间为0或1个元素,就可以结束递归调用
int i = partition(a,l,r);
quick(a,l,i-1);
quick(a,i+1,r);
}
partition函数的实现方式
partition函数是快排的核心部分,它的目的就是将数组划分为小于等于pivot和大于pivot两部分,并返回pivot的正确位置。其实现方法大体有两种,单向扫描版本和双向扫描版本。
单向扫描
int partition(int a[], int l, int r) {
int x = a[l];//选择a[l]作为选定元素
int i = l;
int j = l+1;
for (; j <= r; j++)
if (a[j] < x) //如果a[j]小于x,就将它交换到前面去
swap(&a[++i], &a[j]);
swap(&a[l], &a[i]);//最终将a[l]交换到正确的位置上
return i;
}
双向扫描
int partition(int a[], int p, int r) {
int x = a[p]; //选取a[p]作为选定元素
int i = p + 1;
int j = r;
while (true) {
while (i <= j && a[j] >= x) j--;//从右往左,找到比a[p]小的数
if (i > j) break;
while (i <= j && a[i] < x) i++;//从左往右,找到比a[p]大的数
if (i > j) break;
swap(&a[i++], &a[j--]);
}
swap(&a[j], &a[p]);
return j;
}
比较起来的话,还是单向扫描更简单和直观一些。
单链表的快排实现
由于单链表只能顺序遍历,所以适合使用单向扫描。
struct Node
{
int key;
Node* next;
Node(int nKey, Node* pNext) : key(nKey),next(pNext) {}
};
Node* GetPartion(Node* pBegin, Node* pEnd)
{
int key = pBegin->key;
Node* i = pBegin;
Node* j = p->next;
while(j != pEnd)
{
if(j->key < key)
{
i = i->next;
swap(i,j);//把i和j的值交换一下
}
j = j->next;
}
swap(i,pBegin);//把起始节点的值交换到正确的位置上
return i;
}
void QuickSort(Node* pBeign, Node* pEnd)
{
if(pBeign == pEnd) return;
Node* partion = GetPartion(pBeign,pEnd);
QuickSort(pBeign,partion);
QuickSort(partion->next,pEnd);
}
快速排序的性能特征
平均情况下,快速排序使用大约2NlgN次比较。
最坏情况下,快速排序使用大约N^2/2次比较。
归并排序
原理:基本思路是不断将两个有序的序列合并为一个大的有序序列。具体操作时,首先将序列分为两部分,然后对每一部分进行循环递归,再逐个将结果进行归并。归并排序是一种 稳定排序 。
归并排序依赖于归并操作,归并操作的具体过程如下:
- 申请空间,使其大小为两个已经排序序列之和,然后将待排序数组复制到该数组中
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较复制数组中两个指针所指向的元素,选择相对小的元素放入到原始待排序数组中,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到原始数组末尾
有了归并操作,就可以自顶而下地实现归并排序:将给定序列平分成两个子序列。若两个子序列长度大于1,则再递归划分下去,直到两个子序列都只有1个元素(或者一个有1个元素,另一个没有元素),这时候他们显然是各自有序的。然后进行归并操作,并逐层向上返回。这样层层归并,等到第一层递归调用返回时,再进行最后一次归并,排序就完成了。
与快速排序的联系
快速排序包括一个选择操作过程(确定a[r]的正确位置),后跟两个递归调用。
归并排序的执行过程和快速排序相反,两个递归调用之后是一个归并过程。
代码实现:
Item b[maxN]; //辅助数组
void partition(Item a[],int l,int m,int r) {
int i,j,k;
//将排好序了的两个子数组:a[1~m]和a[m+1~r]放入辅助数组b中
for (i=l;i<=r;i++) b[i]=a[i];
//i和j分别指向两个子数组的开头
i=l;
j=m+1;
k=l;
while(i<=m && j<=r) {
if (b[i]<b[j])
a[k++]=b[i++];
else
a[k++]=b[j++];
}
//如果后半个数组已经全部处理完了
while(i<=m)
a[k++]=b[i++];
//如果前半个数组已经全部处理完了
while(j<=r)
a[k++]=b[j++];
}
void merge(Item a[],int l,int r) {
//当子序列只有1个元素或为空时,返回
if (l>=r) return;
int m = (l+r)/2;
merge(a,l,m);
merge(a,m+1,r);
partition(a,l,m,r);
}
对6 5 3 1 8 7 24 进行归并排序的动画效果如下:
归并操作的视觉效果如下所示:
归并排序的性能特征
归并排序使用大约N lgN 次比较。
归并排序使用与N成正比的额外内存空间。
堆排序
定义:
优先队列:是一种数据结构,其数据项中带有关键字,它支持两种基本操作:向优先队列中插入一个新的数据项,删除优先队列中关键字最大的数据项。
堆有序:如果一棵树中每个节点的关键字都大于或等于所有子节点中的关键字(如果子节点存在),就称树是堆有序的。
堆:本质上是完全二叉树,用数组(vector)表示。
首先定义一个数组,用来表示堆。
int N=0;
vector<int> heap(100005,0);
上浮
当插入一个节点时,为了恢复堆的条件,我们向上移动,需要时交换位置k处的节点与其父节点a[k/2],只要a[k/2]<a[k]
,就继续这一过程,或到达堆顶。
void Up(int k) {
while(k>1 && heap[k/2]<heap[k]) {//是否到根节点了,或者父节点比当前节点小
swap(heap[k/2],heap[k]);
k=k/2;
}
}
下沉
在降低一个节点的优先级时,为了恢复堆的条件,我们向下移动,需要时交换位置k处的节点与其子节点中较大的那个,如果在k处的节点不小于它的任何一个子节点,或到达树底,则停止这一过程。
void Down(int k) {
int j;
while(2*k<=N) {//如果没到达树底
j=2*k;
if(j<N && heap[j+1]>heap[j]) j++;//找到子节点中较大的那个
if(heap[j]<heap[k]) break;//如果父节点比子节点都大,退出
swap(heap[k],heap[j]);
k=j;
}
}
基于堆的优先队列
利用上浮和下沉的堆化操作,可以实现优先队列的插入和删除操作。
插入时,在堆尾加入新元素,然后使用Up来恢复堆的条件。
void insert(int weight) {
N++;
heap[N]=weight;
Up(N);
}
删除顶上元素时,取出该顶上元素(根节点),然后把当前堆的最后一个元素临时放在根节点上,然后使用Down来恢复堆的条件。
int deleteMax() {
swap(heap[N],heap[1]);
N--;
Down(1);
return heap[N+1];
}
堆排序
使用堆对数组a[]进行排序,首先是依次把元素插入到堆中,然后从大到小,依次递减取出当前最大元素,从而完成排序。
int pq[MAX];
int N=0;
void PQsort(int a[],int l,int r) {
int k;
for (k=l;k<=r;k++) insert(a[k]);
for (k=r;k>=l;k--) a[k]=deleteMax();
}
初次建堆的时间复杂度为O(n),删除堆顶元素并维护堆的性质需要O(logn),这样的操作一共进行n次,故最终时间复杂度为O(nlogn)。我们不需要利用额外空间,故空间复杂度O(1)。
topN问题
给定n个元素,要求其中最大/小的N个元素,即为topN问题。
思路:
最大topN问题可以用大小为N的最小堆实现:首先建立起一个大小为N的最小堆;然后从N+1个元素开始直到最后一个元素,插入一个元素,紧接着弹出堆顶元素(最小的那个),最后剩下的堆中元素即为n个元素中最大的N个。
简单修改下上一节的代码:
int pq[MAX];
int N=0;
void PQsort(int a[],int l,int r) {
int k;
for (k=l;k<l+N;k++) //建立大小为N的堆
insert(a[k]);
for(k=l+N;k<r;k++) { //每插入一个新元素,删去顶上元素
insert(a[k]);
deleteMax();
}
for(int i=1;i<=N;i++) //输出这个堆,即topN的解
cout<<pq[i];
}
最小topN问题则类似。