堆原理及其基本应用

本文详细介绍了堆的定义,包括其性质和在数组中的表示。堆的常见操作如上移、下移、插入、删除以及创建堆等进行了阐述,并提供了相应的Pascal代码实现。堆在数据结构中扮演着重要角色,常用于优先队列和排序算法如堆排序。

一、堆的定义:

  一个(二叉)堆是一个几乎完全二叉树,它的每个节点都有满足堆的每个特性:如果v和p(v)分别是它的节点和父节点,那么存储在p(v)中的数据项中的键值不小于(或不大于)存储在v中的数据项的键值。

  

堆的数据结构支持以下运算:

   Delete-max[h]:从一个非空的推H中删除最大键值的数据项并将数据项返回。

   Insert[h,x]:插入x到堆中。

   Delete[h,i]:从堆中删除第i项

   Makehaep[A]:将数组A转换成堆

  这个堆的特性蕴含着:沿着每条从根到叶子的路径,元素的健值以非升序排列:
   a.T的根节点存储在H[1]中。
   b.假设T的节点存储在H[j]中,如果它有左子节点,这个子节点存储在H[2j]中,如果它也有右节点,这个子节点存储在H[2j+1]中。
   c.元素H[j]的父节点如果不是根节点,则存储在H[j/2]中。

堆可以看以一棵完全二叉树,它实际上是一个数组H[1..N]。它有如下性质:对于任何索引j,2<=j<=N,

,如下图:

 

二、堆的运算。
  堆上运算(上移、下移、插入、删除、最除最大值)、创建堆、堆排序。下面通过一个具体的实例来演示这个操作。
 

2.1堆上操作

假定对于某个i>1,H[i]变成了健值大于它父结点的健值的元素,这样就违返的堆的特性,因此这样的数据结构就不成为堆了,要修复堆的特性,需要用Sift-up把新数据项移动合适的位置。即:运算沿着从H[i]到根结点的唯一一条路径,把H[i]移动到合适的位置。在沿着路径的每一步上,都将H[i]与其父节点我建值相比较。

算法如下:

procedure siftup(var h:arr;i:integer);//上移操作,当某个节点值大于其父节点时
 var
  done:boolean;
  t:integer;
 begin
  if i=1 thenexit;//如果要移动的节点是第一个节点,则退出
  repeat
   done:=false;
   ifh[i]>h[i div 2]//同它的父节点比较,如果大于父节点
    thenbegin//则交换
       t:=h[i];
       h[i]:=h[idiv 2];
       h[i div2]:=t;
      end
    elsedone:=true;//否则已成为一个新的堆
   i:=i div2;//回到父节点
  until (i=1)or done;//直到回的根节点或完成堆操作
 end;

2.2堆下运算

假定对于,存储在H[i]的元素的值小于H[2i]和H[2i+1](如果存在的话)中最大的一个,这样就违返的堆的特性,树就不再成为一个堆了。如果要修复堆,要使用Sift_down操作运算把H[i]“渗”到合适的位置上,沿着这条路径每一步,都把H[i]的健值和存储在它的子节点(如果存在)两个健值最大那个相比较。

procedure siftdown(varh:arr;n,i:integer);//下移操作,当某个结点的值小于子结点
 var
  done:boolean;
  t:integer;
 begin
  done:=false;
  if 2*i>nthen exit;//当节点i没有子节点时,则退出
  repeat
   i:=2*i;//左子节点序号
   if(i+1<=n) and (h[i+1]>h[i]) then i:=i+1;//找出健值更大的子节点
   if h[i div2]<h[i]//父结点同较大子节点比较,如果父节点小于较大子节点
    thenbegin//则交换
       t:=h[i];
       h[i]:=h[idiv 2];
       h[i div2]:=t;
      end
    elsedone:=true;//否则堆建好
  until(2*i>n) or done;{直到建好堆或者无子节点}
 end;

 

2.3堆的插入

   为了把元素x插入堆H中,先将堆的大小加1,然后将x添加到堆的末尾,再根据需要,把x上移,直到满足堆的特性。

procedure heapinsert(var h:arr;varn:integer;x:integer);//插入新元素
 begin
  inc(n);//长度增一
  h[n]:=x;//放在最后
  siftup(h,n);//上移操作
 end;

2.4删除元素

   要从大小为n的堆删除元素H[i],可先用H[i]替换H[i],然后将堆大小减1,如果需要的话,将H[i]的值与存储在它父节点和子节点中元素健值的关系,对H[i]做出上移或下移的操作,直到满足堆的特性。

procedure heapdelete(var h:arr;varn:integer;i:integer);//删除某一节点
 var
  x,y:integer;
 begin
  x:=h[i];//取节点值存入x中
  y:=h[n];//取最后一个节点值存入y中
  dec(n);
  if i=n+1then exit;//如果是最后一个节点则退出
  h[i]:=y;//把最后一个节点存入第i个节点中
  if y>=xthen siftup(h,i)//如果比原来的节点大则上移
      elsesiftdown(h,n,i);//否则下移
 end;

2.5删除最大值

这项运算是在一个非空堆H中删除并返回最大健值的数据项。在堆中返回最大健值的元素需要O(1)的时间,因为这个元素是树的根节点。然而由于删除根节点破坏了这个堆,必须要修复这个堆。修复时同删除堆中任意一节点一样,由于删除点为堆的根节点,所以用下移操作。

procedure heapdeletemax(var h:arr;varn,x:integer);//删除根节点
 begin
  x:=h[1];
  heapdelete(h,n,1);
 end;

2.6创建堆

   给出一个有n个元素的数组A[1..n],要创建一个包含这些元素的堆是容易的,可以这样进行:从空堆开始,不断插入每一个元素,直到A完全被子转移成堆为止。因为插入第j个元素用进O(lgj),因此这种方法创健堆的时间复杂度为O(nlgn)。

   有趣的是,可以证明能在O(n)的时间内创建堆,用n个元素来创建堆,实现细节如下,我们知道对应于堆H[1..n]的树节点可以方便地以自顶向下,从左到右的方式从1到n编号。这样编号后,可以用以下的方法,把一棵n个节点的几乎完全二叉树转换成堆H[1..n],从最后一个节点开始(编号为n那个)到根节点(编号为1那个),逐个扫描所有的节点,要据需要,每次将以前的节点为根节点的子树转换成堆。

  首先把数组A[1..n]=(4,3,8,10,11,13,7,30,17,26)转换成堆。图形演示如下:

程序代码:

procedure makeheap(var h:arr;n:integer);//建堆
 var
  i:integer;
 begin
  for i:=n div2 downto 1 do//从非叶子节点开始下移操作
  siftdown(h,n,i);
 end;

 这里就放一个排序的小头堆就好了

  • 
    
const
 maxn=200000;
var
 a,f:array [0..maxn] of longint;
 i,j,n,t:longint;

procedure siftup(i:longint);
var
 c,j,n:longint;
begin

 if f[i]<f[i div 2] then
 begin
  c:=f[i];
  f[i]:=f[i div 2];
  f[i div 2]:=c;
  siftup(i div 2);
 end;

end;

procedure siftdown(x:longint);
var
 c,p,q:longint;
begin

  if (f[x]>f[x*2+1]) and (f[x*2+1]<f[x*2]) and (x<=t) then
  begin
   c:=f[x];
   f[x]:=f[x*2+1];
   f[x*2+1]:=c;
   siftdown(x*2+1);
  end else
  if (f[x]>f[x*2]) and (f[x*2]<=f[x*2+1])  and (x<=t) then
  begin
   c:=f[x];
   f[x]:=f[x*2];
   f[x*2]:=c;
   siftdown(x*2);
  end;
end;


begin
 fillchar(f,sizeof(f),$7f);
 f[0]:=0;
 readln(n);
 for i:=1 to n do
  read(a[i]);
 t:=n;
 for i:=1 to n do
 begin
  f[i]:=a[i];
  siftup(i);
 end;
 while t>1 do
 begin
  write(f[1],' ');
  f[1]:=f[t];
  f[t]:=maxlongint;
  dec(t);
  siftdown(1);
 end;

 writeln(f[1]);
end.


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值