排序时,实际中,待排序的数很少是单独的数值,它们通常是称为记录(record)的数据集的一部分。每一个记录包含一个关键字(key),就是排序中要重排的值。记录的剩余部分有卫星数据(satellite data)组成,通常与关键字是一同存取的。
算法 | 最坏情况运行时间 | 平均情况/期望运行时间 |
插入排序 | Θ(n2) | Θ(n2) |
归并排序 | Θ(nlog2n) | Θ(nlog2n) |
堆排序 | О(nlog2n) | —— |
快速排序 | Θ(n2) | Θ(nlog2n)(期望) |
计数排序 | Θ(k+n) | Θ(k+n) |
基数排序 | Θ(d(n+k)) | Θ(d(n+k)) |
桶排序 | Θ(n2) | Θ(n)(平均情况) |
一、堆
堆排序的时间复杂度是O(nlog2n),堆排序具有空间原址性:任何时候都只需要常数个额外的元素空间存储临时数据,堆排序引入了一种算法设计技巧:使用一种我们称为“堆”的数据结构来进行信息管理。堆不仅用在堆排序中,而且它也可以用来构造一种有效地优先队列。在Java和Lisp中它被引申为垃圾收集存储机制。
如图所示,(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树,树上的每一个结点对应数组中的一个元素。除了最底层外,该树是个完全充满的,而且是从左到右填充。表示堆的数组A包括两个属性:A.length(通常)给出数组元素的个数,A.heap-size表示堆元素(即堆结点)的个数,有多少个堆元素存储在该数组中,这里0≤A.heap−size≤A.length。树的根节点是A[1],给出一个结点的下标i很容易计算其它父节点、左孩子和右孩子。
给出结点i,其父节点为⌊i2⌋ ,左孩子结点为2i,右孩子结点为2i+1,堆的高度log2n
二叉树堆可以分为两种形式:最大堆和最小堆。在这两种堆中,结点的值都要满足堆的性质,最大堆是除了根以外的所有结点i都要满足:A[parent(i)]≥A[i]。也就是说某个结点的值至多与其父节点一样大。因此,堆中的最大元素存放在根结点中。在任一子树中,该子树所包含的所有结点的值都不大于该子树根结点的值。同理,最小堆的组织方式正好相反,最小堆是指除了根结点以外的所有结点i都有:A[parent(i)]≤A[i]。最小堆的最小元素存放在根节点中。在堆排序算法中,我们使用的是最大堆。最小堆通常用于构造优先队列。
二、调整堆
MAX-HEAPIFY是用于维护堆性质的重要过程。它的输入为一个数组A和一个下标i。在调用MAX-HEAPIFY的时候,我们假定下标i的以左子树left(i)和右子树right(i)为根节点的二叉树都是最大堆,但这时A[i]有可能小于其孩子,这样就违背了最大堆的性质,MAX-HEAPIFY通过让A[i]的值在最大堆中逐级下降,从而使得以下标i为根节点的子树重新遵守最大堆原则。
MAX-HEAPIFY(A,i)://最大堆的调整的伪代码
1 l=LEFT(i)//i的左节点
2 r=RIGHT(i) //i的右节点
3 if l<=A.heap-size and A[l]>A[i]
4 largest=l
5 else largest=i
6 if r<=A.heap-size and A[r]>A[largest]
7 largest=r
//3-7行代码求出A[i],A[LEFT(i)]和A[RIGHT(i)]的最大的,并将其下标存储在largest中。
8 if largest !=r
9 exchange A[i] with A[largest]
10 MAX-HEAPIFY(A,largest)
在程序的每一步中,从A[i],A[LEFT(i)]和A[RIGHT(i)]中选出最大的,并将其下标存储在largest中。如果A[i]是最大的那么以i为结点的子树已经是最大的堆,程序结束运行。否则,最大元素是i的某个孩子节点,则交换A[i]和A[largest]的值。从而使得i及其孩子都满足最大堆的性质。在交换后,下标为largest的结点的值就是原来的A[i],于是以该结点为根的子树又可能会违法最大堆的性质。因此需要递归调用MAX-HEAPIFY。
三、建堆
我们可以用自低向上的方法利用过程MAX-HEAPIFY把一个大小为n=A.length的数组A[1..n]转换为最大堆。子数组A[⌊n2⌋+1…n]中的元素都是树的叶结点。每一个叶结点可以看成只包含一个元素的堆。第n个结点的父节点为⌊n2⌋,因此在初始数组情况下我们需要从第⌊n2⌋ 结点开始递归调用MAX-HEAPIFY调整堆,一直到第1个结点,则建堆完成。关键点是,n个结点的完全二叉树,最后一个结点是第⌊n2⌋个结点的子树。
BUILT-MAX-HEAP(A):
1 A.heap-size=A.length
2 for i=[A.length/2] downto 1//向下取整
3 MAX-HEAPIFY(A,i)
从第5个结点开始调整,依次4,3,2,1直到建堆完成。
四、堆排序算法
初始时,堆排序算法利用BUILD-MAX-HEAP将数组A[1..n]建成最大堆,其中n=A.length。因为数组中的最大元素总是在根结点A[1]中,通过把它和A[n]进行互换,我们可以让该元素放到正确的位置。这时候,如果我们从堆中去掉结点n(这一操作可以通过减少A.heap-szie的值来实现),剩余的结点中,原来根的孩子结点仍然是最大堆,而新的根节点可能违背最大堆的性质。为了维护最大堆的性质,我们调用MAX-HEAPIFY(A,1),从而在A[1..n-1]上构造一个新的最大堆。排序算法会不断重复这一过程,直到堆的大小从n-1降到2。
HEAPSORT(A)
1 BUILD-MAX-HEAP(A)
2 for i=A.length downto 2
3 exchange A[1] with A[i]
4 A.heap-size=A.heap-size-1
5 MAX-HEAPIFY(A,1)
五、优先队列
堆排序算法是一个优秀的算法,堆排序的一个最常应用为:最大优先队列和最小优先队列。最大优先队列应用:一个为在共享计算机系统的作业调度,最大优先队列记录将要执行的各个作业以及它们之间的相对优先级,当一个作业完成或中断后,调度器调用EXTRACT-MAX从所有的等待作业中,选出具有高优先级的作业来执行。在任何时候调度器可以调用INSERT把一个新的作业加入到队列中来。
c++STL中提供了一个优先队列priority-queue。
template <class T,class Container=vector<T>,class Compare=less<typename Container::value_type> > class priority_queue
//T:Type of the elements.Aliased as member type priority_queue::value_type.
//Container:Type of the internal underlying container object where the elements are stored.Its value_type shall be T.
//Compare:The expression comp(a,b), where comp is an object of this type and a and b are elements in the container
priority_queue调用 STL里面的 make_heap(), pop_heap(), push_heap() 算法实现,也算是堆的另外一种形式。priority_queue 对于基本类型的使用方法相对简单。他的模板声明带有三个参数:
priority_queue<Type, Container, Functional>:其中Type 为数据类型, Container 为保存数据的容器,Functional 为元素比较方式。Container 必须是用数组实现的容器,比如 vector, deque 但不能用 list。STL里面默认用的是 vector。
比较方式默认用 operator< , 所以如果你把后面俩个参数缺省的话,优先队列就是大顶堆,队头元素最大。如果要使用小顶堆,则一般要把模板的三个参数都带进去。STL里面定义了一个仿函数greater<>,对于基本类型可以使用这个仿函数声明小顶堆。对于自定义类型则必须自己重载operator<或者自己写仿函数。
六、示例程序
K链表合并 Merge k Sorted Lists:
#include <iostream>
#include <vector>
#include <queue>
#include <cstdlib>
#include <ctime>
using namespace std;
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
struct compare
{
bool operator()(ListNode* list1,ListNode* list2)
{
return list1->val>list2->val;
}
};
ListNode *mergeKLists(vector<ListNode *> &lists)
{
priority_queue<ListNode*,vector<ListNode*>,compare>Myqueue;
int mLength=lists.size();
if(mLength==0) return NULL;
if(mLength==1) return lists[0];
ListNode* headPtr=new ListNode(-1);
ListNode* movePtr=headPtr;
ListNode* tempElement=NULL;
for (int i=0;i<mLength;i++)
{
if(lists[i]!=NULL) Myqueue.push(lists[i]);
}
while(!Myqueue.empty())
{
tempElement=Myqueue.top();
movePtr->next=tempElement;
movePtr=movePtr->next;
Myqueue.pop();
if(tempElement->next!=NULL) Myqueue.push(tempElement->next);
}
return headPtr->next;
}
int main()
{
int max=1000;
srand(time(NULL));
vector<ListNode*> lists;
ListNode* head=NULL;
for (int i=0;i<10;i++)
{
ListNode* temp=new ListNode(1+rand()%max);
lists.push_back(temp);
}
head=mergeKLists(lists);
return 0;
}