Project1
标题:
[FSL]Class版二叉堆(2010-09-23 更新)
[打印本页]
作者:
沉影不器
时间:
2010-9-17 21:03
提示:
作者被禁止或删除 内容自动屏蔽
作者:
429259591
时间:
2010-9-17 21:04
沉影大神的东西,收藏!
作者:
逸豫
时间:
2010-9-17 21:28
RGSS的效率再优化也就那样了- -
恩……说实话比起维持堆和直接sort的效率区别真的那么大么- -
看了下代码……这不还是用数组了么……Ruby米有指针还真是不方便- -
作者:
沉影不器
时间:
2010-9-17 23:18
提示:
作者被禁止或删除 内容自动屏蔽
作者:
紫苏
时间:
2010-9-18 15:40
本帖最后由 紫苏 于 2010-9-18 15:46 编辑
使用面向对象的思想的话,自然得用类而不是模块了 ^^
关于初始化:
这里用的是 Array#sort 对数组进行排序,其实并非最优的算法。
可以试想,二叉堆同级的节点并不需要从左到右的顺序(即同级节点顺序可以是混乱的),而排序会保证同级节点的顺序,这显然是多此一举,可以避免,能节省不少步骤。目前最流行的方法是先把数据放在数组中,不用考虑顺序,然后在初始化二叉堆时用删除二叉堆元素的那个 max-heapify 算法(基本上可以看作这里的 dequeue 的一个归纳算法),从二叉堆底部有子节点的节点开始迭代,一直到头部,整个过程完了以后就得到了一个合法的二叉堆了。
Ruby 数组使用的是快速排序,平均时间是 O(n log n),而使用 max-heapify 的二叉堆建立算法,网上能找到证明了时间是 O(n) 的文章。刚才在 Ruby 层也写了一个 BinaryHeap,实测时毫不意外地发现 max-heapify 的方法比 sort 的方法慢得多。Ruby 1.8 低效率的 GC 和无优化纯解释的性质也不是新闻了,用 Ruby 1.8 写出来的这种数据结构底层接口,性能是比较糟糕的,不可能和 Array#sort 这样的 C 函数比效率,如果是需要大量使用的类库,还得靠 DLL 接口提升性能。后来实测了一下,在 DLL 里相同的 max-heapify 算法确实比 quick sort 的方法快了不少,大概也就是 log n 的因子,而且同时也试了一下用循环把元素添加到二叉堆的方法,比 quick sort 慢不了多少(而在 Ruby 中这么做是和 quick sort 有天壤之别的,虽然两个理论上都是 O(n log n)),沉影不妨一试
关于 enqueue:
这里其实可以不用每次都交换变量。平行赋值用起来倒简洁,但解释器在解释的时候还不是只能用临时变量来实现。以前写插入排序时发现了一个有趣的现象:如果每次把未排序的元素取出时通过交换变量的值插入到排好序的元素中,其效率远远不及用一个临时变量记录这个需要插入的元素,然后让排好序的元素依次向需要插入的元素的方向移动,直到腾出了需要插入的元素的位置为止,之后可以直接把元素放于这个位置。这个现象站在汇编层的角度来想也是合理的:每次通过临时变量交换变量都需要使用三个寄存器,使用三次 mov 指令,而位移只需要开头、结尾的各一次的 mov,中间每次用来位移的 mov 只有一次,代码长度的差别还是比较明显的。这里我们需要让新加入的元素插入到堆中一个满足条件的位置,其思想和插入排序是一致的:让应该放置在新插入的元素下面的父节点依次下移,直到找到一个应该在新插入元素上面的父节点为止,这时就可以直接把新插入的元素放到最后一个下移的父节点之前的位置。
关于 dequeue,这行会影响效率:
@data.unshift @data.pop
复制代码
unshift 会调用 memove 拷贝内存重新分配空间,而之后的 pop 则会调用 xrealloc 重新分配内存。这两个操作用经典的算法理论分析都是 O(n) 时间,虽然实际上是很快的,但并非快到可以忽视的地步,毕竟 dequeue 还是可能会频繁调用的。这里可以避免使用 shift 和 unshift 方法,直接 pop,把结果放到 arr[0]
另外吐槽一下二叉堆的基本运算名称数量,一个简单的 `add' 就能概括的事,非要取一大堆 sift-up、bubble-up、heapify-up 之类的名字显得专业 >.>
话说这里用的 enqueue 和 dequeue 使得结构看起来基本和优先队列没有区别了……
附一份刚写的,初始化部分比较慢,原因之前说了:
class BinaryHeap
def initialize(*args, &cmptr)
@cmptr = cmptr
@heap = args.clone
i = (@heap.size - 1) >> 1
while i >= 0
max_heapify(i)
i -= 1
end
end
def add(elem)
i = @heap.size # 当前节点索引
loop do
j = (i - 1) >> 1 # 父节点索引
if j < 0 || @cmptr.call(elem, @heap[j]) <= 0 # 顺序正确
@heap[i] = elem
break
else # 顺序不正确
@heap[i] = @heap[j]
i = j
end
end
self
end
def shift(idx = 0)
ret = @heap[idx]
len = @heap.size
if len > 1
@heap[idx] = @heap.pop
max_heapify(idx)
elsif 1 == len then @heap.clear
end
ret
end
def max_heapify(idx)
len = @heap.size
loop do
i = (idx << 1) + 1 # 左子节点
j = i + 1 # 右子节点
max = idx
max = j if j < len &&
@cmptr.call(@heap[j], @heap[max]) > 0
max = i if i < len &&
@cmptr.call(@heap[i], @heap[max]) > 0
if max != idx
@heap[idx],@heap[max] =
@heap[max],@heap[idx]
idx = max
else
break
end
end
end
def to_s(indent = 8)
helper = lambda do |idx, acc|
if idx >= @heap.size then ''
else
child_base = idx << 1
helper.call(child_base + 2, acc + 1) +
' '*indent*acc + @heap[idx].to_s + "\n" +
helper.call(child_base + 1, acc + 1)
end
end
helper.call(0, 0)
end
def <<(elem)
add(elem)
end
end
DATA = 10
data = Array.new(DATA)
DATA.times do |i|
data[i] = rand(DATA<<1)
end
bh = BinaryHeap.new(*data) { |a, b| a <=> b }
print bh
bh << 1 << 3 << 0 << -3 << 0
print bh
bh.shift
print bh
bh.shift
print bh
复制代码
作者:
沉影不器
时间:
2010-9-18 20:20
提示:
作者被禁止或删除 内容自动屏蔽
作者:
小幽的马甲
时间:
2010-9-18 21:46
本帖最后由 小幽的马甲 于 2010-9-18 21:47 编辑
不过RGSS会有什么要用到堆?Dijkstra或是Prim么……
大家都习惯Array.sort!了呢= =
不过我希望能有人写个菲波那契堆{:nm_9:}
作者:
紫苏
时间:
2010-9-19 01:08
回复
小幽的马甲
的帖子
可以用来实现原地的堆排序 o.o
不过最大的应用还是实现优先队列,有了优先队列后可以用于离散的事件、消息队列(模拟各种带优先权的队列模型)等,Dijkstra、A*、SMA* 也都是其应用;Prim 可以用二叉堆来表示图,但追求效率就应该用 Fibonacci 堆
至于和 sort 的不同,在于堆比较重视最值,所以省略了很多不需要的排序步骤,各种运算过程都是比排序高效的
作者:
沉影不器
时间:
2010-9-23 16:31
提示:
作者被禁止或删除 内容自动屏蔽
作者:
紫苏
时间:
2010-9-23 20:35
嗯,但指针是连续内存...
shift 确实只是增加指针:
00480 /*
00481 * call-seq:
00482 * array.shift -> obj or nil
00483 *
00484 * Returns the first element of <i>self</i> and removes it (shifting all
00485 * other elements down by one). Returns <code>nil</code> if the array
00486 * is empty.
00487 *
00488 * args = [ "-m", "-q", "filename" ]
00489 * args.shift #=> "-m"
00490 * args #=> ["-q", "filename"]
00491 */
00492
00493 VALUE
00494 rb_ary_shift(ary)
00495 VALUE ary;
00496 {
00497 VALUE top;
00498
00499 rb_ary_modify_check(ary);
00500 if (RARRAY(ary)->len == 0) return Qnil;
00501 top = RARRAY(ary)->ptr[0];
00502 ary_make_shared(ary);
00503 RARRAY(ary)->ptr++; /* shift ptr */
00504 RARRAY(ary)->len--;
00505
00506 return top;
00507 }
复制代码
不过就像我说的,unshift 和 pop 则会在缓冲区容量不够的时候重新分配内存:
00509 VALUE
00510 rb_ary_unshift(ary, item)
00511 VALUE ary, item;
00512 {
00513 rb_ary_modify(ary);
00514 if (RARRAY(ary)->len == RARRAY(ary)->aux.capa) {
00515 long capa_inc = RARRAY(ary)->aux.capa / 2;
00516 if (capa_inc < ARY_DEFAULT_SIZE) {
00517 capa_inc = ARY_DEFAULT_SIZE;
00518 }
00519 RARRAY(ary)->aux.capa += capa_inc;
00520 REALLOC_N(RARRAY(ary)->ptr, VALUE, RARRAY(ary)->aux.capa);
00521 }
00522
00523 /* sliding items */
00524 MEMMOVE(RARRAY(ary)->ptr + 1, RARRAY(ary)->ptr, VALUE, RARRAY(ary)->len);
00525
00526 RARRAY(ary)->len++;
00527 RARRAY(ary)->ptr[0] = item;
00528
00529 return ary;
00530 }
复制代码
这里的 REALLOC_N 就是包装了 xrealloc 的一个宏;下面还有一个的 MEMMOVE 用来把数组整体右移一位,之后空出来的第一位被设为 unshift 的参数。一般类似这种插入删除的运算,都需要 O(n),只不过由于按块转移内存,所以效率损失微乎其微
pop 的:
00444 VALUE
00445 rb_ary_pop(ary)
00446 VALUE ary;
00447 {
00448 rb_ary_modify_check(ary);
00449 if (RARRAY(ary)->len == 0) return Qnil;
00450 if (!FL_TEST(ary, ELTS_SHARED) &&
00451 RARRAY(ary)->len * 2 < RARRAY(ary)->aux.capa &&
00452 RARRAY(ary)->aux.capa > ARY_DEFAULT_SIZE) {
00453 RARRAY(ary)->aux.capa = RARRAY(ary)->len * 2;
00454 REALLOC_N(RARRAY(ary)->ptr, VALUE, RARRAY(ary)->aux.capa);
00455 }
00456 return RARRAY(ary)->ptr[--RARRAY(ary)->len];
00457 }
复制代码
也需要在适当时候重新分配内存,只不过这次是缩减容量
to_s 那个是写过无数次的二叉树 ASCII 化 + -90度旋转...
作者:
沉影不器
时间:
2010-9-23 22:10
提示:
作者被禁止或删除 内容自动屏蔽
作者:
紫苏
时间:
2010-9-24 07:19
本帖最后由 紫苏 于 2010-9-24 07:29 编辑
嗯,是 C 写的
不知 unshift 你是怎么测的?九楼那样测一次能看出来的话,反而不正常了,可以这样测:
DATA = 10000
a=[]
DATA.times { |i| a << i }
b=[1]
t1=Time.now
DATA.times { a.unshift(0) }
p Time.now-t1
t2=Time.now
DATA.times { b.unshift(0) }
p Time.now-t2
复制代码
一万以内都有明显的差别
-------------------
传递块那个其实是在搞函数式编程时养成习惯了 = = 实际上会影响效率的,因为 Ruby 块符合闭包的上下文绑定特性,这是个昂贵的过程。如果用默认传递的块(即不使用 & 保存到参数,使得块只能对当前方法的局部上下文起作用)的话会好一点
汗,打到这里才想起可以传递 Method 对象,因为没有绑定……但用起来自然是没有块惬意了
欢迎光临 Project1 (https://rpg.blue/)
Powered by Discuz! X3.1