为防止广告,目前nocow只有登录用户能够创建新页面。如要创建页面请先登录/注册(新用户需要等待1个小时才能正常使用该功能)。

块状链表

来自NOCOW
跳转到: 导航, 搜索

目录

[编辑] 块状链表的意义

有时候我们需要设计这样一种数据结构:它能快速在要求位置插入或者删除一段数据。

先考虑两种简单的数据结构:数组和链表

数组能够在O(1)的时间内找到所要执行操作的位置,但无论是插入或删除都要移动之后的所有数据,复杂度是O(n)的。

链表能够在O(1)的时间内插入和删除一段数据,但是在寻找操作位置时,却要遍历整个链表,复杂度同样时O(n)的。

这两种数据结构各有优缺点,我们尝试将两种数据结构融合成一个全新的数据结构:块状链表。

为了体现块状链表的优越性,我们再举个例子:

先搬道题目出来:

编程实现一个文本编辑器的功能,操作有:移动光标,删除光标后XX个字符,在光标后添加一段文本,输出光标后XX个字符。

首先,我们来讨论一下暴力写法。

No.1:数组:

用数组实现这些操作,我们看下总时间复杂度分析:

移动光标:O(1*times(<=50000))

删除光标后XX个字符:O(总字符数(<=2MB)*times(<=4000))

在光标后添加一段文本:O(总字符数*times(<=4000))

输出光标后XX个字符:O(P)

//备注:P<=3MB, 输出光标后XX个字符在所有算法中复杂度相同,接下来将会略去

在询问次数较多,并且添删字符较多是必定会超时

No.2:链表:

移动光标:O(n*times(<=50000))

删除光标后XX个字符:O(总字符数)

在光标后添加一段文本:O(总字符数)

现在,轮到块状链表登场了:

移动光标:O(sqrt(n)*times(<=50000))

删除光标后XX个字符:O(sqrt(总字符数))

在光标后添加一段文本:O(sqrt(总字符数))

{均摊平衡树SplayTree可以做到所有操作均摊O(Log(总字符数))}

[编辑] 块状链表的具体介绍

块状链表是这样的:从总体上来看,维护一个链表,链表中的每个单元中包含一段数组,以及这个数组中的数据个数。

每个链表中的数据连起来就是整个数据。

设链表长度为a,每个单元中的数组长度是b。

无论是插入或删除,在寻址时要遍历整个链表,复杂度是O(a);

对于插入操作,直接在链表中加入一个新单元,复杂度是O(1);

对于删除操作,会涉及多个连续的单元,如果一个单元中的所有数据均要删除,直接删除这个单元,复杂度是O(1),如果只删除部分数据,则要移动数组中的数据,复杂度是O(b)。

总的复杂度是O(a+b)。

因为ab=n,取a=b=√n,则总的复杂度是O(√n)。


问题是如何维护a和b大致等于√n?

必须维持块状链表中每块的大小在区间[sqrt(n)/2,sqrt(n)*2]中,否则,块状链表会退化。维护可以通过块的合并与分裂完成,合并复杂度,同样是O(sqrt(n))


这样,维护操作并不会使总复杂度增加。

最终得到一个复杂度是O(√n)的数据结构。


[编辑] 块状链表的实现

以noi2003的editor题目为例

'''C++版''' {这是保存光标位置的做法}

/******************************
            块状链表
    For NOI2003 :     Editor
    Author:        DarkRaven
 *****************************/
#include<iostream>
#include<cmath>
#define MAX 2900
struct block{//Type of block
    int nos;//Number of elements in this block
    char em[MAX];//elements 
    block *be,*su;//previous&successor
}*first;
struct cursor{//Type of cursor
    int n;//The postion of cursor(in one block)
    block *ll;//The block the cursor in
}cur;
int tot,n;//tot:the total number of elements,n:the number of operations
inline void clean(block *op){//Clean a new block
    op->nos=0;
    memset(op->em,0,sizeof op->em);
    op->be=NULL;
    op->su=NULL;
}
inline void Spilt(block *a,int newsize){//Break a block into two blocks,one's size equal to 'newsize',and another's equal to 'a->nos-newsize'
    if(!newsize)return;
    int tmp=a->nos;
    a->nos=newsize;
    block *tps=a->su;
    a->su=new(block);
    clean(a->su);
    a->su->be=a;
    if(tps){
        a->su->su=tps;
        a->su->su->be=a->su;
    }
    a->su->nos=tmp-newsize;
    block *tt=a->su;
    for(int i=newsize+1;i<=tmp;i++)tt->em[i-newsize]=a->em[i];
}
inline void Merge(block *a){//Merge a & a->su
    if(a->su==NULL)return ;
 int tmp=a->nos;
    for(int i=1;i<=a->su->nos;i++)a->em[a->nos+i]=a->su->em[i];
    a->nos+=a->su->nos;
    block *oo=a->su;
 if(cur.ll==oo){
  cur.ll=cur.ll->be;
  cur.n+=tmp;
 }
    a->su=a->su->su;
    delete(oo);
    if(a->su)a->su->be=a;
}
inline void Balance(){//Make these blocks' size balance
    block *k=first;
    int kk=(int)sqrt(tot);
    for(;k!=NULL;){
        for(;k->nos < kk/2 || k->nos > 2*kk;){
            if(k->nos<kk/2){//the block is too small?
                if(k->su)Merge(k);
                else break;
            }
            else if(k->nos>kk*2){//the block is too big?
    Spilt(k,(k->nos)>>1);
    if((cur.ll==k)&&(cur.n>k->nos)){
                    cur.ll=k->su;
                    cur.n-=k->nos;
                }
    k=k->su;
    }
        }
  k=k->su;
    }
}
inline void Insert(block *lk,int x,int k){//Insert text behind the cursor
     block *oo=new(block);
    clean(oo);
    block *gg=oo;
    int rr=k;
    tot+=rr;
    int bt=(int)sqrt(tot);
    for(int i=0;i<rr;i++){
     char gg;
  scanf("%c",&gg);
  for(;(gg>126)||(gg<32);scanf("%c",&gg));
  oo->em[++oo->nos]=gg;
  if((oo->nos>=bt)&&(i<rr-1)){
   oo->su=new(block);
   clean(oo->su);
   oo->su->be=oo;
   oo=oo->su;
  }
 }
 if(x){
  Spilt(lk,x);
 }
 else {
  if(!cur.ll->be){
   block *jj;
   jj=first;
   first=new(block);
   clean(first);
   first->su=jj;
   jj->be=first;
  }
  cur.ll=cur.ll->be;
  cur.n=cur.ll->nos;
  lk=lk->be;
 }
 block *tmp=lk->su;
 lk->su=gg;
 oo->su=tmp;
 if(oo->su)oo->su->be=oo;
 lk->su->be=lk;
 cur.ll=lk->su;
 cur.n=0;
 Balance();
}
inline void Remove(block *lk,int x,int num){ //Delete 'num' elements behind the cursor
 if(x){
  Spilt(lk,x);
  lk=lk->su;
 }
 else {
  if(!cur.ll->be){
   block *jj;
   jj=first;
   first=new(block);
   clean(first);
   first->su=jj;
   jj->be=first;
  }
  cur.ll=cur.ll->be;
  cur.n=cur.ll->nos;
 }
 tot-=num;
 int ttt=num;
 block *tmp;
 block *ii;
 for(tmp=lk;tmp&&((num-tmp->nos)>=0);tmp=ii){
  ii=tmp->su;
  num-=tmp->nos;
  delete(tmp);
 }
 if(num&&tmp){
  Spilt(tmp,num);
  cur.ll->su=tmp->su;
  cur.ll->su->be=cur.ll;
  if(cur.ll->be)cur.ll->be->su=cur.ll;
 }
 else {
  cur.ll->su=tmp;
  if(cur.ll->be)cur.ll->be->su=cur.ll;
  if(cur.ll->su)cur.ll->su->be=cur.ll;
 }
 Balance();
}  
inline void Print(block *lk,int x,int num){//print 'num' elements behind the cursor
 block *cp=lk;
 for(;num-(cp->nos-x)>0;cp=cp->su){
  for(int i=x+1;i<=cp->nos;i++)printf("%c",cp->em[i]);
  num-=(cp->nos-x);
  x=0;
 }
 for(int i=1;i<=num;i++)printf("%c",cp->em[i+x]);
 printf("\n");
}
inline void prev(){//cursor move forward
 cur.n--;
 if(cur.n<0){
  cur.ll=cur.ll->be;
  cur.n=cur.ll->nos-1;
 }
}
inline void next(){//cursor move backward
 cur.n++;
 if((cur.n>=cur.ll->nos)&&(cur.ll->su)){
  cur.ll=cur.ll->su;
  cur.n-=cur.ll->be->nos;
 }
}
inline void move(int k){//move the cursor to the postion 'k'
 cur.ll=first;
 for(;(cur.ll)&&(k-cur.ll->nos>0);cur.ll=cur.ll->su)
 k-=cur.ll->nos;
 cur.n=k;
}
int main(){
 freopen("editor.in","r",stdin);
 freopen("editor.out","w",stdout);
 first=new(block);
 clean(first);
 char str[100];
 cur.ll=first;
 cur.n=0;
 scanf("%d\n", &n);
 int k;
 for (int i=0;i<n;i++) {
  scanf("%s", str);
  switch (str[0]) {//deal with operations
      case 'M' : scanf("%d", &k); move(k); break;
            case 'I' : scanf("%d", &k); Insert(cur.ll,cur.n,k); break;
            case 'D' : scanf("%d", &k); Remove(cur.ll,cur.n,k); break;
            case 'G' : scanf("%d", &k); Print(cur.ll,cur.n,k);; break;
            case 'P' : prev(); break;
            case 'N' : next(); break;
        }
    }
 
    fclose(stdin);
    fclose(stdout);
}

Pascal版

{维护了一个Jason域用于二分查找光标位置(Jason[i,1]为1..i块的长度和,Jason[i,2]为第二块的位置)} {其实不用每次都重新算一遍,不过我懒得写了,估计也快不了多少} {这里要是写“块状跳表”“块状平衡树”的话有点秃}

{1.发现Maxm开大了的话会慢很多 不知道为什么} {2.Union操作,我怀疑Jason那个块状链表也是不能保证 块的长度都是合法的} {3.一定要注意等号 就是这个等号 我昨天Debug了一夜} {4.Jason770如是说,想好了再编程} {5.AnsiString貌似是链表没法Watch,AnsiString的效率要比String和WideString高很多,也省空间} {6.个人怀疑维护Jason会变慢,因为我偷懒,没维护Prev} {7.个人怀疑我的输出要比Jason770天牛慢} {8:Insert的最后一句一定要Union(x),Union(Last)的话会死得很惨} {9.该程序还有改进余地} {============================膜拜Jason770天牛Orz=================================} {by cjf00000} {============================膜拜cjf天牛和AlNo3天牛===============================} {Morenan弱菜}

program editor;
 
const maxm = 10000; Cjf= 3000;
      fin = 'editor.in'; fout = 'editor.out';
type bartype=record
      next:longint;
      data:ansistring;
end;
var a:array[0..maxm]of bartype;
      Jason:array[0..maxm,1..2]of longint;
n,k,Cursor,Open,TotBar,Head:longint;
str,temp:ansistring;
ch:char;
 
Procedure ReadSpace;                                      //读入多余字符
begin
    read(ch);
    while ch<>' ' do read(ch);
end;
 
Function Search(Cursor:longint):longint;                 //返回Cursor所在的是第几个块
var l,r,mid:longint;
begin
    l:=1;r:=totbar;
    while l+1<r do begin
      mid:=(l+r) div 2;
      if Jason[mid,1]<cursor then l:=mid
      else r:=mid;
    end;
    if l+1=r then if Jason[l,1]>=cursor then exit(l) else exit(r);
    exit(l);
end;
 
Procedure ReadForInsert;                                //读入字符串
begin
    str:='';
    while k>0 do begin
      readln(temp);
      str:=str+temp;
      dec(k,length(temp));
    end;
    k:=length(str);
end;
 
Procedure Ccopy;                                        //Ccopy=Cut
begin
    if length(str)>Cjf then begin
      A[open].data:=Copy(str,1,Cjf);
      Delete(str,1,cjf);
    end
    else begin
      A[open].data:=Copy(str,1,length(str));
      str:='';
    end;
end;
 
Procedure CalcNewJason;
Var Now,count:Longint;
Begin
    Fillchar(Jason,Sizeof(Jason),0);
    Now:=Head;Count:=0;
    While Now<>0 do begin
      Inc(Count);
      Jason[Count,1]:=Jason[Count-1,1]+Length(A[Now].Data);
      Jason[Count,2]:=Now;
      Now:=A[Now].Next;
    End;
    Totbar:=Count;
End;
 
Procedure Union(Start:longint);
Var Now,Sum,Count:Longint;
Begin
    Now:=A[Start].Next;Count:=0;Sum:=Length(A[Start].Data);
    While (Sum+Length(A[Now].Data)<=Cjf)and(Now<>0) do begin         //合并字串
      Sum:=Sum+Length(A[Now].Data);
      A[Start].data:=A[Start].data+A[Now].data;
      Now:=A[Now].Next;
      Inc(Count);
    end;
    A[Start].Next:=Now;
    CalcNewJason;
end;
 
Procedure InsertStr;
Var ssfzzzc,x,y,count,last,ra224:longint;
Begin
    ssfzzzc:=Search(Cursor+1);
    x:=Jason[ssfzzzc,2];
    y:=Cursor+1-Jason[ssfzzzc-1,1];
    If x=0 then begin
      x:=1;
      Inc(Open);Inc(Totbar);
      Head:=Open;
    end;
 
    ReadForInsert;
    count:=length(str) div Cjf+1;
    if length(str) mod Cjf>0 then inc(count);last:=x;   //预处理
    inc(Totbar,count);
 
    Inc(Open);inc(count);Ra224:=Open;                   //将Y及后面的字符新开一段
    A[Open].data:=Copy(A[x].data,y,length(a[x].data)-y+1);
    A[Open].Next:=A[x].Next;
    Delete(A[x].data,y,length(a[x].data)-y+1);
 
    count:=0; Last:=x;
    while length(str)>0 do begin                        //存储刚插入的字符串
      inc(count);Inc(Open);
      CCopy;
      A[Last].next:=Open;
      Last:=Open;
    end;
 
    A[Last].next:=Ra224;
    Union(X);                 
End;
 
Procedure DeleteStr;
Var count,ssfzzzc,sum,now,x,y:longint;
Begin
    ssfzzzc:=Search(Cursor+1);                         //ssfzzzc表示光标在第几块
    x:=Jason[ssfzzzc,2];
    y:=Cursor+1-Jason[ssfzzzc-1,1];
 
    If k<=Length(A[X].Data)-y+1 then begin
      Delete(A[x].Data,y,k);
    end
    else begin
      Sum:=Length(A[x].Data)-y+1;Delete(A[x].Data,y,Length(A[x].Data)-y+1);
      Now:=A[x].Next;Count:=0;
      While Sum+Length(A[Now].Data)<k do begin       //删中间的块
        Inc(count);
        Inc(Sum,Length(A[Now].Data));
        Now:=A[Now].Next;
      end;
      Delete(A[Now].Data,1,k-sum);
      A[x].Next:=Now;
      Totbar:=Totbar-count;
    end;
    Union(x);
End;
 
Procedure Get;
Var SsfzZzc,x,y,count:Longint;
Begin
    ssfzzzc:=Search(Cursor+1);                         //ssfzzzc表示光标在第几块
    x:=Jason[ssfzzzc,2];
    y:=Cursor+1-Jason[ssfzzzc-1,1];
 
    count:=0;
    While (count<k) do begin
      inc(count);
      write(A[x].data[y]);
      if y<length(A[x].data) then inc(y) else begin
        x:=A[x].Next;
        y:=1;
      end;
    end;
    writeln;
End;
 
Procedure Zhouye;
Var k:longint;
begin
    k:=head;
    while A[k].next<> 0 do begin
      writeln(length(a[k].data));
      k:=a[k].next;
    end;
end;
 
Procedure main;
var i:longint;
begin
    assign(input,fin);reset(input);
    assign(output,fout);rewrite(output);
    readln(n);
    Open:=0;TotBar:=0;Cursor:=0;Head:=0;
    for i:=1 to n do begin
      read(ch);
      case ch of
        'N':begin
          readln;Inc(Cursor);
        end;
        'P':begin
          readln;Dec(Cursor);
        end;
        'M':begin
          ReadSpace;readln(k);Cursor:=k;
        end;
        'I':begin
          ReadSpace;readln(k);InsertStr;
        end;
        'D':begin
          ReadSpace;readln(k);DeleteStr;
        end;
        'G':begin
          ReadSpace;readln(k);Get;
        end;
      end;
    end;
    close(input);close(output);
end;
 
begin
    Main;
end.

[编辑] 参考资料

北极天南星 数组+链表=块状链表 [1]

lightxianjian 块状链表 [2]

cjf00000 关于Noi2003 Editor(块状链表) [3]

个人工具