Meta-Interpretation

Code, better explained.

[编程之美]寻找最大的k个数

| Comments

《编程之美》上的一道题: wiki链接; 其实在July的文章里有更加深刻的分析,我这里只是用最小堆的方式实现一遍,体会和总结一下。

分析

  • 这个问题,只要找到“最大的K个数”,但并不要求这K个数之间排序;
  • 而且如果K很小的话,完全可以采用选择排序的方式,达到O(nk)的复杂度;
  • 若K不小,则可利用这“只能容纳K个数的最小堆”的方法来做;时间复杂度是O(nlogk);用“堆”这种结构来解决此问题,很好地理解和利用了堆的特性:
    1. 找最大的K个数要使用最小堆,是因为最小堆总能最方便地剔除“最小的元素”,那么最后剩在堆中的都是最大的K个元素了;反之,若要找“最小的K个元素”则应该使用最大堆
    2. 二叉堆的同层节点之间是没有顺序的,这恰好符合我们的题目“不要求K个数之间排序”的需求,少了K个数之间的排序则能够减少不必要计算

程序

#include <stdio.h>

// defines the heap structure
typedef struct {
    int size, last_pos;
    int* data;
} Heap;

void into_min_heap(int node, Heap* heap);
void adjust_min_heap(Heap* heap, int cur_index);

// find the max k digits
void max_k(int len, int array[], int k, int rst[]) {
    // build min-heap on rst while iterating array:
    // worst time complexity: n * log k
    int i;
    Heap heap = { k, -1, rst };
    for (i = 0; i < len; i++) {
        into_min_heap(array[i], &heap);
    }
}

void into_min_heap(int node, Heap* heap) {
    if (heap->last_pos == -1) {
        heap->data[0] = node;
        heap->last_pos = 0;
    } else {
        if (node < heap->data[0]) {
            return;
        } else {
            if (heap->last_pos + 1 < heap->size) {
                heap->data[heap->last_pos + 1] = node;
                heap->last_pos++;
            } else {
                heap->data[0] = node;
                adjust_min_heap(heap, 0); // log k
            }
        }
    }
}

void adjust_min_heap(Heap* heap, int cur_index) {
    int c1_index = (cur_index + 1) * 2;
    int c2_index = (cur_index + 1) * 2 + 1;
    if (c1_index < heap->size) {
        if (heap->data[cur_index] <= heap->data[c1_index]) {
            if (c2_index < heap->size) {
                if (heap->data[cur_index] <= heap->data[c2_index]) {
                    return;
                } else {
                    int tmp = heap->data[cur_index];
                    heap->data[cur_index] = heap->data[c2_index];
                    heap->data[c2_index] = tmp;
                    adjust_min_heap(heap, c2_index);
                }
            }
        } else {
            int tmp = heap->data[cur_index];
            heap->data[cur_index] = heap->data[c1_index];
            heap->data[c1_index] = tmp;
            adjust_min_heap(heap, c1_index);
        }
    }
}

int main(int args, char** argv) {
    int k = 3;
    int rst[k];
    int arr[10] = { 1, 20, -35, 4, 7, 11, 19, -5, 0, 18 };
    max_k(10, arr, k, rst);
    printf("The max k numbers are: ");
    int i;
    for (i = 0; i < k; i++) {
        printf("%d ", rst[i]);
    }
    return 0;
}

总结

还是要理解各种常用数据结构的特性及其意义,遇到特定的问题时才好能够正确地选择出最能适合需求的这种结构。

跟踪递归函数路径的方法

| Comments

有些时候,需要追踪递归函数的路径;比如:递归地遍历一棵树,当找到符合要求的节点时,打印出从根节点到此节点的路径。

这种时候,若子节点不持有到父节点的指针,则需要对路径进行随时地记录,以便需要时能够获取;

  • 一种方式就是建立“栈”结构,在每次递归时将当前节点压栈,并且记得在递归调用过后立刻出栈
  • 另一种方式就是直接利用函数递归调用的“调用栈”(也就是传参)了。

例如有一个题目:输入一个整数和一棵二元树。 从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径。 打印出和与输入整数相等的所有路径。

程序较简单,就是递归遍历,查找节点相加数值等于预期数值的情况:

# define the data-structure first
class Node(object):
    def __init__(self, left=None, value=None, right=None):
    self.value = value
    self.left = left
    self.right = right

root = Node(Node(Node(None, 4, None), 5, None), 10 , Node(None, 12, Node(None, 7, None)))

found = []
def findPath(tree, value, sm=0, paths=[]):
    if tree.value + sm > value:
    return
    elif tree.value + sm == value:
    paths.append(tree.value)
    found.append(paths[:])
    return
    else:
    paths.append(tree.value)
    if tree.left != None:
        findPath(tree.left, value, tree.value + sm, paths)
    if tree.right !=None:
        findPath(tree.right, value, tree.value + sm, paths)
    paths.pop()

findPath(root, 22)
print found

其中值得注意的是:

paths.append(tree.value)

和:

paths.pop()

这两行; 在递归方法前后进行对应地push和pop是正确记录路径必不可少的步骤。

其实,像上述这种每次递归调用之前都push,每次调用之后都pop的情况,其栈的层次、深度、变化规律跟函数调用栈是完全一致的,那么其实就完全可以考虑直接把父节点扔给下一层递归函数:

findPath(tree.left, value, tree.value + sm, tree)

其实,findPath的函数签名现在就变成了:

findPath(树, 预期值, 当前和, 父节点)

这样一来,其实路径上的每一步,都是记录在调用栈上的(就算子节点没有父节点的指针,函数参数也直接把父节点带过来了,也就是在调用栈中),当程序想要输出路径时,也完全能够将路径还原。

[面试中的算法]求子数组的最大和

| Comments

此问题由v_JULY_v整理发布并发表于blog上, 版权归原作者所有。

输入一个整形数组,数组里有正数也有负数。 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。 求所有子数组的和的最大值。要求时间复杂度为 O(n)。 例如输入的数组为 1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为 3, 10, -4, 7, 2, 因此输出为该子数组的和 18

分析:

贪心算法能在此问题的任意一个子问题上找到最优解(最优子结构),所以可以用贪心算法解决。

[面试中的算法]设计包含min函数的栈

| Comments

此问题由v_JULY_v整理发布并发表于blog上, 版权归原作者所有。

问题描述:定义栈的数据结构,要求添加一个 min 函数,能够得到栈的最小元素, 要求函数 min、push 以及 pop 的时间复杂度都是 O(1)

分析:

主要是抓住栈的“还原现场”的能力 —— 由于随着元素的加入与弹出,“最小元素”随时可能变化;在栈的每一个状态,都要维护一个当前最小元素的记录;随着push或pop,“最小元素”也跟着更新或还原到上一次的状态;所以,“最小元素”应该随着栈元素被记录在栈的每一层。

[面试中的算法]把二元查找树转变成排序的双向链表

| Comments

此问题由v_JULY_v整理发布并发表于blog上, 版权归原作者所有。

问题描述:输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。要求不能创建任何新的结点,只调整指针的指向

比如二叉搜索树:

       10
      /   \
    4      12
   / \    /  \
  3   5 11    13

输出应为: 3, 4, 5, 10, 11, 12, 13

分析:

处理树状结构很容易想到递归,而二叉搜索树其实恰好是已经排序好的一个结构,而要把它变成数组,只需“中序遍历”即可: 3,4,5,10,11,12,13

[翻译]Python性能诀窍

| Comments

以下文字译自http://wiki.python.org/moin/PythonSpeed/PerformanceTips

这个页面提供了一些帮助改善你的python程序性能的一些点子或小技巧。
自从我在大约1996年编写了第一个”fast python”页面以来,Python在许多关键的方面有了改变,这意味着一部分内容必须要调整。我将其整合到Python wiki中从而希望其他人能帮助维护它。

你应该始终针对你的应用和你打算使用的、特定的python实现来尝试这些小技巧而不是盲目地相信一种方法比另一种更高效。更多的细节请参考profiling一节。

另外自从这个页面最初编写以来,新出现的包例如Cython, Pyrex, Psyco, WeavePyInline, 都能通过更容易地将性能要求较高的代码推到C或机器码上去实现从而大幅地改进你的程序的性能。

概览:优化那些需要优化的地方

你只能在程序首先给出正确的结果之后才能知道哪里致使你的程序慢了,所以先保证程序正确,再跑程序看是否慢。 当发现慢了,性能监控能发现程序的哪部份消耗了大部分的时间。一套全面、方便运行的测试能保证后面的优化没有改变你的程序的正确性。
简而言之:

  1. 把程序调正确。
  2. 测试程序的正确性。
  3. 如果发现慢,分析它的性能。
  4. 优化。
  5. 从第’2’步开始重复。

某些优化方法刚好就是一个好的编程风格,所以你应该在学习这门语言的时候就学习它们。其中一个例子就是移动那些在循环内或循环外都不会改变的值计算。

[翻译]”AI-class”中的概率论必备知识

| Comments

在2011年末的ai-class中出现许多关于概率论的问题,其实这里有一篇神帖非常精彩:A challenge to the Over achievers who really understand Probability 精华在于评论

总结起来,ai-class中所用到的概率论,可以总结为下面几条:

  1. P(A,B) = P(A|B)P(B) // bayes theorem ie; conditional probability
  2. P(A) = P(A,B’) + P(A,B) // total probability : let B’ = not B
  3. P(A) = P(A|B)P(B) + P(A|B’)P(B’) // total probability
  4. P(A1,A2,A3,A4) = P(A1|A2,A3,A4)P(A2|A3,A4)P(A3|A4)P(A4) // chain rule - can be extended to n variables

第一个,说是贝叶斯公式,可能一眼看不出来,但这正是其神奇之处,只要稍加变化:

P(A, B) = P(A|B)P(B) --> 
P(B|A)P(A) = P(A|B)P(B) --> 
P(A|B) = P(B|A)P(A) / P(B)    // 这就是“正规”的贝叶斯公式

个人认为,比起“正规”的贝叶斯公式,第一个这种变种会好记得多,并且和“条件概率”结合起来,加深理解了贝叶斯公式的本质(本质上它就是条件概率的巧妙关系);

第二、三个,都是“全概率公式”,只不过第三个比第二个多变化了一次而已(用条件概率公式变化了一下);

第四个,是“链条法则”,其实一步一步自己慢慢利用上述的其它公式也能变幻出这个结果,但每次都这么做过于繁复,所以直接当作一个深刻的结论记下来会有很大好处。

OK,上面这些总结足够——秒杀ai-class里面的任何一个概率论问题,主要用在贝叶斯网络里面;包括后面开课的CS 373: PROGRAMMING A ROBOTIC CAR,所有的概率论相关计算都能通过上述总结来导出; 一般过程就是:将原公式通过上述总结进行变换,向已知条件“靠拢”,最后求值;

当然, 还有一小部分内容叫做“D-Separation”,懂得了它之后,才敢说“秒杀”贝叶斯网络,这是后面需要介绍的一点点内容: (待续…)