在单向链表中拆除某个节点的方法
离开Turbo Pascal的5、6年时间里很少再写有关于数据结构的东西了。
最近自己写服务器程序,终于又爽了一回数据结构和算法。只是好多东西都快忘记,时常还需要翻书辅记。在Delphi的IniFile里看到一个HashTable实现,其中的Find方法用到了指向指针的指针,C中指针满天飞,而在Pascal语言中,指针的指针还是不常使用的。仔细一看,原来这是自己曾经常用的一种单向链表维护方法,可用于快速去除单向链表中某个节点。这里记述一下,当作对从前知识的复习。
首先看以下关键代码:
PPHashItem = ^PHashItem;
PHashItem = ^THashItem;
THashItem = record //Hash表的节点,整个Hash表是一个动态数组
Next: PHashItem; //对于发生碰撞的节点,它们会自动串接成为一个链表。
Key: string;
Value: Integer;
end;
Buckets: array of PHashItem;
function TStringHash.Find(const Key: string): PPHashItem;
var
Hash: Integer;
begin
Hash := HashOf(Key) mod Cardinal(Length(Buckets));
//取模,保证Hash范围,在本文的讨论中可以先忽略它。
Result := @Buckets[Hash];
//碰撞链表的第一个节点位置
while Result^ <> nil do //开始遍历,获取需要的节点
begin
if Result^.Key = Key then
//Result.Key, 如果不使用指针来表示,其实是THashItem.Next.Key,
//这里就是技巧所在,通过提前检测下一个节点,避免单独保存上级节点指针
Exit
else
Result := @Result^.Next;
end;
end;
//该方法则是对Find方法的一个使用
procedure TStringHash.Remove(const Key: string);
var
P: PHashItem;
Prev: PPHashItem;
//该指针的指针变量名已经显示出他的实际作用,
//他指向的其实是我们实际希望删除的节点的上级节点。
begin
Prev := Find(Key);
P := Prev^;
if P <> nil then
begin
Prev^ := P^.Next;
//所以这里直接把上级节点的Next指针指向要被删除的节点的Next指针指向的下级节点
Dispose(P);
//直接释放P。
end;
end;
上面的简单注释肯定无法让人理解,下面我举个例子来说明。假设我们的数据结构如下:
数组Buckets:[A,B,C,D,E,F....Z]
| |
1 2 (数字代表THashItem )
|
3
|
4
我们需要删除3时,Find方法返回的指向指针的指针Prev实际指向的是 1.Next(请注意,是Next指针本身,而不是Next指针所指向的THashItem内存),所以P := Prev^,就相当于P指向了Next所指向的节点3(这里P才是指向Next指针指向的内存地址),Prev^ := P^.Next也就表示把Next指针指向的地址,变更为3.Next所指向的地址。于是,节点三从链表A中被删除,接下来就可以释放节点3所占用的内存了,Dispose(p)。
由此可见,采用这种方法,省去了在单向链表中单独记录上级指针的麻烦,这也就不难理解Prev命名的原因。
顺便,又想到了现在的程序开发。以我们公司的.NET程序员为例(特别是习惯于数据库开发的程序员),对于数据结构和算法知识的掌握真的很差,不少人根本不明白基本数据结构知识。当然,这也和大学里的教育有很大关系,很多大学教师,自己就没办法把这门有一定难度的课程说清楚,或者它们自己本身就不清楚(想想自己的大学时跟数据结构老师的争执,还是略为不爽)。软件开发领域算是工程应用,对语言本身掌握的不熟练,导致学生更难理解数据结构知识。而很多数据结构知识又是从实践应用中总结得到。自己学数据结构时,不觉吃力,很大原因是在那之前,我已经拿Turbo Pascal写了许多代码,看到数据结构里的线性表,发现那就是自己常用的技巧。可见在计算机学习的初期,放手让学生拥有充足的代码实战经验,才是继续提升他们能力的根本。