再解树形数据结构(一)

{

最近做了很多有意思的数据结构问题

在这里小结一下 顺便介绍一下数据结构问题的基本解决方法

由于以前零零散散介绍过几部分数据结构

所以标题就是 再解数据结构了

由于碰到的问题基本都和树形结构脱不了干系

这几段文章都是围绕树形数据结构

}

=================吐槽的分割线====================

在这神奇的世界上有个神奇的地方

叫做Online Judge 简称OJ 是ACMER OIER打发寂寞空虚的地方

在众多OJ中有两个神奇的OJ: SPOJ和SGU

SGU是一个卡内存的地方 很多题目的内存少的可怜

而SPOJ是一个卡常数的地方 很多很多理论复杂度很好的程序经常被判定为TLE

原因就是测评机的速度不是一般的速度

据神牛考证 以前可以AC的题 现在在SPOJ都面临AC不能的境遇

可见SPOJ测评机的速度不仅慢 而且是一种动态的慢 越来越慢

一句老话说得好 黑发不知勤学早 白首方悔读书迟 啊

用在神奇的SPOJ上真是一点也不为过

不过 和SGU相比 SPOJ倒是不卡内存的

几百M的内存任你开 通常SPOJ上不会出现MLE这个错误

取而代之的是TLE 因为开那么多内存

再加上SPOJ测评机的神速 足以让测评机判定为TLE了

与国外众多OJ相比 中国本土的OJ相对而言很和谐

测评机速度快 网页也快 社会主义国家的OJ果然不一样

=================吐槽的分割线====================

由于SPOJ的神奇......

在SPOJ上过一些有意思的题 也成了神奇的事情

本来很有意思的问题 解决的也比较顺利 应该算是一件很爽的事

但是SPOJ却常常卡一个正确程序的常数

辛苦了很久换来一个黄色的TLE 实在不是什么好玩的事情

但是 值得肯定的是 SPOJ上确实有很多值得一做的题

所以这里介绍一下 不被神奇的SPOJ囧到的一个技巧...尽量使用指针!

=============================================

一.指针与数组

一般的树形数据结构都离开不了指针

In computer science, a pointer is a programming language data type whose value refers directly to (or "points to") another value stored elsewhere in the computer memory using its address.

(From: http://en.wikipedia.org/wiki/Pointer_%28computing%29 )

指针变量存储一个内存地址

非常形象地 指针变量的作用就好比一个导引方向的指针

利用指针 我们可以非常自由地组织内存中的数据

这无疑给我们实现各种数据结构都提供了方便

数组 则是另一种常用的数据类型

In computer science, an array data structure or simply array is a data structure consisting of a collection of elements (values or variables), each identified by at least one index. An array is stored so that the position of each element can be computed from its index tuple by a mathematical formula. 

(From: http://en.wikipedia.org/wiki/Array_data_structure )

用数组存储一段数据 可以让我们方便地检索第K个数据是什么

另外 作为一种几乎所有语言都支持的基本数据类型 利用数组的程序也可以很方便地调试

=============================================

二.利用数组和指针实现数据结构

以二叉排序树为例 介绍两种基本的实现方式

一种就是最最和谐的指针实现方式 通常教材上都是介绍的这种实现

一个二叉排序树的节点通常有三个域 即左儿子 右儿子 键值

而且保证左子树所有节点的键值都小于根节点的键值 右子树所有节点的键值都大于根节点的键值

type	BST=^BSTnode;
	BSTnode=record
		l,r:BST;
		n:longint;
		end;
var	root:BST;
begin
new(root);
root^.l:=nil; root^.r:=nil;
root^.n:=1;
end.

由于指针变量是一个内存地址 所以对于我们调试程序增加了难度

正是由于指针变量的调试不方便 在对某些数据结构不熟悉的情况下

我们可以使用一种折中的办法 数组模拟指针

注意到 数组下标可以和内存地址等价 我们可以用数组来模拟指针操作

所以上面的代码可以改写成这样

type	BSTnode=record
		l,r:longint;
		n:longint;
		end;
var	pool:array[1..100]of BSTnode;
	tt:longint;
begin
tt:=0;
inc(tt); root:=tt;
pool[root].l:=0; pool[root].r:=0;
pool[root].n:=1;
end.

为了方便书写 我们通常不是用record类型来定义

而是写成下面的样子 这样虽然结构松散 但是更加方便

var	l,r,n:array[1..100]of longint;
begin
tt:=0;
inc(tt); root:=tt;
l[root]:=0; r[root]:=0;
n[root]:=1;
end;

=============================================

三.比较快的写法

上面两种写法 其实速度都不是很快 下面介绍一种我常用的速度还可以的写法

首先有一个结论 引用一次指针变量比引用一次数组变量要快

这是因为: 数组在内存中是连续的一段 通常计算机存储数组都是存储一个数组头部地址

然后引用数组元素是把下标作为偏移量 用数组头部地址加偏移量 得到元素地址

所以 引用一次数组元素比 直接引用指针变量 多一次加法

加法是一种不可忽略的基本运算 尤其是在多次调用的时候

而实现数据结构 通常都会多次的引用节点信息 由此带来的就是多出上百万甚至是上千万级别的加法

所以我们要尽量使用指针来代替数组模拟 这样可以使程序速度得到明显的提高

然而 使用指针也有瓶颈 就是每次给节点分配地址的操作New函数

借鉴上面数组模拟指针的实现 可以得到一个比较好的解决方法

分配数组的空间是分配一段连续内存 通常要比动态分配很多节点要快

只要预先开好一个内存池 然后利用尾指针++ 就可以不断得到新的节点

而且速度非常快 这样就可以得到一个比较完美的数据结构通用实现方法

基本代码如下:

type	BST=^BSTnode
	BSTnode=record
		l,r:BST;
		n:longint;
		end;
var	BSTpool:array[0..100]of BSTnode;
	tt,root,null:BST;
begin
null:=@BSTpool[0];
null^.l:=null; null^.r:=null;
tt:=null;

inc(tt); root:=tt;
root^.l:=null; root^.r:=null;
root^.n:=1;
end.

其中null是一个哨兵节点 可以减少对于空指针的特殊判断

@就是取变量地址的操作 以内存池的第一个节点作为哨兵节点

初始尾指针就是内存池的第一个节点

=============================================

四.指针与数组模拟指针的实践测试

为了更有力地证明指针和数组模拟指针的效率差距

我作了实际测试 测试代码是两棵写法一致 但是分别使用了指针和数组模拟指针的平衡树

测试结果如下:

其中输入流由程序内部伪随机函数提供 输出流省略

所以测试直接针对两种不同实现的平衡二叉树

实测中 数组模拟指针实现的平衡二叉树比指针实现的速度要慢1/2左右

作为这一部分的序篇 主要介绍一些常数优化的方法

之后介绍一些有意思的数据结构问题

posted on 2011-06-03 14:26  Master_Chivu  阅读(4218)  评论(3编辑  收藏  举报

导航