标签归档: IT

老夫聊发少年狂——加拿大IT求职经历

以前在日本时,经过那么多年的努力和不断尝试以及生活的际遇,我逐渐找到了一个比较niche的领域,就是FinTech,或者更直接的说法,就是Quant IT,因为一方面我喜欢做程序员,另一方面我又喜欢股票投资。但自从到了加拿大的大温哥华地区后,就难以找到类似的职位了,于是只好去做普通的IT工作。这样,就离开了自己的最佳区域。虽说我们要勇于走出自己的舒适区域,可是如果舒适区域就是最佳区域呢?唯一可以聊以自慰的是,我至少在没到这里的时候就已经找到了一份程序员的工作,比其他一些生活多年都未能找到自己的专业工作的移民来说,情况还算好一些。

在经过一番闯荡,跳槽到目前这家公司后,因为公司希望我们以合同工的形式加入来降低雇佣风险,我就注册了自己的公司,老板和打工仔都是我自己。后来,公司的人事部门发出了一个招聘启事,我看到里面的条件好像照着我的简历写的一样,可能是暗示想把我转变成正式员工吧?职位是开发经理。我觉得来这里后生活乱糟糟的,没心情去做经理来管理别人,同时也比较喜欢目前的“皮包公司”的状态,就没申请。后来一个印度女同事坐到了这个职位,开会时我有点惊讶地发现,她手下管理的人比我预计的还多不少。不过,她开始时好像有点缺乏安全感,对中国传统文化——包括糟粕部分——还是有点了解的我,当然可以设法避免被她“杯酒释兵权”地清算,不过还是有点大意了。我还以为会从外面招一个来呢。

现在的工作是为银行编写和维护一个用于开户的云端系统。我所对应的客户是加拿大的一家银行。不知道这家银行本身如何,但跟我们有关的人比较讨厌,就是对IT毫无经验却又毫不讲理地蛮横的那么一个人。这种性格,当然是一个女性经理——没有歧视女性的意思,但根据生活中的经验,显然这种性格的女人的比例要比男人高得多。后来,那边也感觉这样整天跟我们针锋相对的也不是回事,就又调来另一位号称懂技术的人,让她处于该经理和我们之间,充当缓冲层。她的确能明白点,但显然距离懂技术还是有一段的距离。总之,从我们的角度看,就是客户似乎永远也不能满意,我有一种Gaslighting被害的感觉。好在后来我对该银行对我们的评分几乎都无视了,反正我的雇主又不是他们,而是这家开发软件的公司。我的一位同样是女性的同事则不满于此,在几个月前辞职了。不管怎么说,我本身也没有喜欢受虐的M倾向,于是也开始考虑寻找下一份工作。

大温这边IT工作的情况是,有几家美国的大公司(如亚马逊、微软等),其他的就是一大堆的中小公司。亚马逊的一个猎头找到我,问我是否有兴趣应聘他们的一个职位。我在当时那种有点郁闷的心情下,难免容易受到这类诱惑,就开始准备面试。开始时需要在网上做一个在线技术测试。我只好捡起数据结构等多年不看的内容(把一些用于快速复习的内容归结在了这几篇文章中)。但是,由于我并非赋闲在家,而是还有一份工作,因此,没有太多的时间用于准备这个,所以要求该猎头把考试时间延后一些,她也同意了。到时间后,我发现那个链接已失效,联系猎头,她说她会处理的。但后来就不再有音信了。可能是她大意了,但系统不允许在短期内重新申请了吧?总之,这次机会就无疾而终了。

然后,是一家创立于美国硅谷的在线家具公司A。也是让我先做一个网上测试,我费了些时间和心力做完了,记得里面有一个颇有些难度的关于图的遍历的题。结果,他们说给我的题错了!又发来另一个链接,而且是另一个在线技术测试公司的题。这次的题居然简单得多(太坑爹了)。后来,进入在线面试阶段,他们认为我缺少一些诸如微服务等的经验,以及缺乏诸如“高屋建瓴”的那种经验(以前在投行的时候,经常是我自己从设计到实现一整套系统好不好?就非得自己只说些空话大话,让其他人去努力实现,才算高屋建瓴吗?),就面试失败了。

另一份工作是北美的一个很大的电器连锁店B的IT工作。同样是先做一个在线测试,令我惊讶的是,这些题居然很简单。之后是在线面试,结果一个对技术很热衷的华人小伙居然让我当场写一段关于树的遍历的代码,也是能在线编译并执行的那种。好在我写了出来。后来就是跟诸如管理层的面试,感觉不太好,令我恶心(不怪该公司,是我自己)的是,居然顺着他们的所谓企业文化说了一些有违自己个性的话。虽说是高级程序员的工作,但他们也是认为我缺乏诸如微服务的经验,也缺乏一些管理经验,就提出了一个低一些的工资。我当然没必要到那里去拿比现在更低的工资,因为我毕竟不是年轻人了,一方面就算能增长一些经验,对我来说也没多少年的用处了,另一方面,对于年龄较大的人来说,如果公司给的工资低,就说明他们不会拿你当回事儿的,我又何必去妄自菲薄呢?于是就拒绝了这份offer。

结果就是,目前还是在从事着这份工作。有时候发现,最好的经常就是现在所拥有的,但经常因为“这山看那山高”的心理而做一些没必要的挣扎,这样反而让自己的情况每况愈下。退一步反思一下自己,我觉得自己是无意中被“套路”了,认为就得去那种有名的公司才对得起自己这么多年来的努力和自己的经验。可是,在加拿大抑郁的生活中,偶尔想起来统计一下自己的资产状况,才更加意识到自己竟会如此犯傻。总之,这就是我“老夫聊发少年狂”的一次经历。其实,维持着目前的工作,在这份工作被解约后再去找一份新工作,而把因换工作而浪费的时间(准备和参加面试,熟悉新的环境,等等)用于更有意义的事情上,才是更好的选择。

另外,我发现在加拿大(至少是在大温地区)找工作,有点像是个玄学问题。上面的几次经验还多少能说得清,但经常是,我自己也不知道为什么能得到一些工作,而不能得到另一些。在我做目前这份工作之前,我面试过另一家加拿大的公司,还没开始面试,他们就认为我的工资过高(后来网上了解了一下,很多人都抱怨他们大量找新移民,且把工资压得很低),说如果我同意应聘“中级程序员”的话,他们会很高兴面试我。我告诉猎头,那就这样吧。结果面试中级程序员,他们也认为我不合格。我不相信他们能比我高多少(甚至是,他们就高吗?),但对这些公司,没必要花时间去想为什么,就抛之脑后即可。我在从事目前这份工作1年多了,在一次招聘会上,见他们还是在招这个职位。后来,又有猎头给我介绍工作,我隐隐觉得公司名有点耳熟,就查了一下以前的Email,发现正是这家公司,就立即拒绝了。

还有一个硅谷的公司,不是那种高大上的公司,而是拉一伙人去做外包项目的公司。一个项目跟加拿大和美国的电力系统有关,我去应聘,也是在线测试等比较简单地通过了,之后在线面试莫名其妙地失败了。有可能是我要的对他们来说有点高了?

同样的,我也参与过公司招聘新人的技术面试。我不像其他公司那样喜欢问一些技术难题,好像难不倒应聘者就没面子一样。我一般是问很基本的问题,如果真的是程序员,不用特别准备也应该很容易地答上来。结果,面试了三四个人,就有一个是我的前同事的人能勉强全答上来。而且我的问题清单从来没用完过,那位前同事在答同样多的题的情况下比其他应聘者都好。但一个team lead却认为其他几个应聘者更好,因为他们说得比较流畅。我的反对意见是,他们根本连这么基本的问题都做不出来,怎么指望他们能来这里实际做出什么贡献呢?结果几个都被拒绝了……这也能从一个侧面看出,通过一个面试是多么的困难。

二维网格问题的解法

经常有面试问题是二维网格问题,比如迷宫问题、迷宫最短路径问题、N皇后问题等等。求解时主要有以下要素:

  • 遍历:对每个格子(位置)进行遍历。
  • 搜索相邻的格子。

总体的搜索可以有深度优先(DFS)和广度优先(BFS)之分。

根据我自己的经验,在有限的时间内解答这类问题时,要注意以下几点:

二维数组的行列顺序

二维数组是按照先行后列的顺序存储的:

void func(int[][] data) {
    if (data == null || data.length = 0 || data[0].length == 0) {
        // ...
        return;
    }

    int rows = data.length;
    int cols = data[0].length;

    for (int r = 0; i < rows; ++r) {
        // 或者更好的写法是:c < data[r].length
        // 但这类问题一般都是个矩形区域,因此把cols提前定好,代码更简洁
        for (int c = 0; c < cols; ++c) {
            data[r][c] = 0;
        }
    }

    // ...
}

循环变量名称

循环变量名称似乎根本不是个问题,一般会用i和j来表示。但在有限时间而且缺乏IDE帮助debug的情况下,对于二维格子问题,很容易引起思维混乱。我自己就有一次在线测试时,因为把行和列弄颠倒了,导致时间到了还无法把程序调通。后来自己“复盘”的时候,才发现原因所在。

因此,建议把变量名称弄得一看就懂:

  • 行数和列数:rows,cols
  • 表示行和列的循环变量名:r,c

例如:

for (int r = 0; r < rows; ++r) {
    for (int c = 0; c < cols; ++c) {
        data[r][c] = 0;
    }
}

搜索相邻的格子

一般我会按东西南北四个方向编写:

// N
if (r > 0) {
    backtrack(r - 1, c);
}

// S
if (r < rows - 1) {
    backtrack(r + 1, c);
}

// W
if (c > 0) {
    backtrack(r, c - 1);
}

// E
if (c < cols - 1) {
    backtrack(r, c + 1);
}

这样看似很有效率,但很啰嗦,而且在情急之下容易犯错。我经常会因为拷贝粘贴的代码中没改完全而导致程序不能通过,而之后要用肉眼去观察一遍来找出错误的话,很费时间。因此,在效率没有多少区别的情况下,程序越简短越好。

//                                        N   S   W   E
private static final int[] rowOffsets = {-1,  1,  0,  0};
private static final int[] colOffsets = { 0,  0, -1,  1};

private void backtrack(int[][] data, int[][] visited, int r, int c) {
    // ...

    int rows = data.length;
    int cols = data[0].length;
    BiPredicate<Integer, Integer> isValid = (row, col) ->
            row >= 0 && row < rows && col >= 0 && col < cols;

    // 尝试搜索相邻的格子
    for (int i = 0; i < rowOffsets.length; ++i) {
        int r1 = r + rowOffset[i];
        int c1 = c + colOffset[i];

        if (isValid.test(r1, c1) && !visited[r1][c1]) {
            backtrack(data, visited, r1, c1);
        }
    }

    // ...
}

有人喜欢在枚举相邻格子时,按东南西北的顺序转一圈。在我看来似乎没什么必要。最重要的是简单且不易弄错,所以我都是选择按北、南、东、西的顺序轮转。当然,每个人习惯不同,可以选择自己最舒适的顺序。

总结:对于编程来说,最好是从一开始就不要把bug放进去,否则要找出来就需要花费成倍的时间。因此,要选择各种一看就懂的方式写代码,防止写到中间自己都迷糊了。另外,为了避免查找bug的时间过长,应该尽量写简短的代码(当然不是那种为了短而短的十分缺乏可读性的那种)。

用于算法题的常用Java工具

Array & List

// 排序
int[] array = {3, 4, 1, 2, 5};
Arrays.sort(array);    // T = O(n * log2(n))
List<Integer> list = Arrays.asList(array);
Collections.sort(list);  // 使用ArrayList的话,T = O(n * log2(n))
list.sort(null);         // 上面的函数调用这个函数,其内部实现方法是先把列表转换为数组再排序
                         // 因为如果是链表,直接排序的时间复杂度为O((n ^ 2) * log2(n))。

// array和list转换
List<String> list = Arrays.asList(array);
String[] array = list.toArray(new String[0]);

// max & min
Map<String, Integer> map = ...;
// 找出具有最大value值的key
String max = Collections.max(
        map.entrySet(),
        Comparator.comparingInt(Map.Entry<String, Integer>::getValue)).getKey();
// 或者
max = Collections.max(
        map.entrySet(),
        Map.Entry.comparingByValue()).getKey();
// 这些其实没什么必要,面试时记不住就用循环解决

// string
String str = "aaa bbb ccc";
String[] parts = str.split(" ");      // {"aaa", "bbb", "ccc"}
String[] parts1 = str.split(" ", 2);  // {"aaa", "bbb ccc"}
String[] parts2 = str.split(" ", -1); // -1的意思是不要删除最后面的空字符

另外,Java的Collection由于只接受对象形式的元素(就是说不能用基本数据类型的元素),加上其维护代码的开销,性能往往比数组差。比如int[]比ArrayList<Integer>要快,尤其是在比拼算法所用时间的情况下尤为明显。

Stack

“Stack”是Java中关于栈的旧API。现在应该使用Deque。LIFO。

Deque<String> stack = new ArrayDeque<>();
stack.push("abc");
String s1 = stack.peek();
String s2 = stack.pop();

Queue

普通的队列,在Java中一般是以LinkedList的方式实现的。Queue常用于广度优先搜索算法。FIFO。

Queue<String> queue = new LinkedList<>();
queue.offer("abc");       // enqueue
String s = queue.poll();  // dequeue

Priority Queue

Priority Queue常被用来在不排序的情况下取最大的或最小的几个值。如果有n个数据元素,要取前k个(k<n)最小的元素,用排序法的话T=O(n * log2(n)),而用Priority Queue的话,则为T=O(n * log2(k)),这样就能快一些。方法是往queue里逐个放入数据元素,如果queue的长度大于k,则把一个元素移出(也就是最大的那个或最小的那个,取决于Priority Queue的设定方式。这样,剩下的就是k个最小的或最大的元素。

Priority Queue的时间复杂度:enqueue和dequeue操作为O(log2(k)),查看(peek)最前面的元素为O(1)。

PriorityQueue<Elem> queue = new PriorityQueue<>(Comparator.comparingInt(Elem::getValue)); 

for (Elem elem : list) {
    queue.offer(elem);
    if (queue.size() > k) {
        queue.poll();
    }
}

上面的代码中的queue.poll()会把Elem.getValue()返回值最小的元素移出。要想移出最大的,则改为Comparator.comparingInt(elem -> -elem.getValue())。

另一个应用的例子:合并k个有序列表。一般的算法为取各列表的第一个元素,找到最小的那个,加入新列表,然后把最小元素的下一个元素作为它原所属列表的第一个元素。不断重复,直到用完所有列表的元素。这样做的话,T=O(N * k),其中N为所有列表中的元素总数,k为列表个数。因为每次从k各元素中取最小值为O(k),而对所有元素都要进行一次这样的操作,故为O(N * k)。

可以用Priority Queue进行优化:把各列表的第一个元素加入queue;取出最前面的元素加入新列表,并把它的下一个元素加入queue。这样的话,每次enqueue和dequeue操作为O(log2(k)),对所有元素做这种操作,总体的时间复杂度T=O(N * log2(k)),比上述的O(N * k)要快。

public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) {
        return null;
    }

    if (lists.length == 1) {
        return lists[0];
    }

    PriorityQueue queue = new PriorityQueue<>(Comparator.comparingInt(n -> n.val));

    for (ListNode node : lists) {
        if (node == null) {
            continue;
        }

        queue.offer(node);
    }

    ListNode dummyHead = new ListNode(0);
    ListNode prev = dummyHead;

    while (!queue.isEmpty()) {
        ListNode node = queue.poll();

        prev.next = node;
        prev = node;

        if (node.next != null) {
            queue.offer(node.next);
            node.next = null;
        }
    }

    return dummyHead.next;
}

题外话:另一种十分简单的解法是Divide & Conquer:合并两个有序列表(按第一种思路)的时间复杂度是O(n),n是两个列表中的元素总和。可以把k个列表两两分组,并将其合并成一组更长的列表;然后继续两两分组合并,直到最后只剩一个列表为止。这看起来跟体育比赛分组类似,没什么特别的,但观察一下的话,一共有log2(k)轮的分组,每轮都要遍历所有N个元素,因此,时间复杂度也是O(N * log2(k))。

public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) {
        return null;
    }

    if (lists.length == 1) {
        return lists[0];
    }

    int i = 0;
    List mergedList = new ArrayList<>();

    // 两两分组合并。merge2()的作用是普通的合并两个有序列表。
    while (i < lists.length) {
        ListNode node1 = lists[i];

        if (++i < lists.length) {
            mergedList.add(merge2(node1, lists[i++]));
        } else {
            mergedList.add(node1);
        }
    }

    return mergeKLists(mergedList.toArray(new ListNode[0]));
}

Map

HashMap经常用来提高程序的效率。写入(put)、查找(contains)和取值(get)都为O(1)。这里有一些简单写法:

// 计数:记录相同词语出现的次数
Map<String, Integer> map = new HashMap<>();

for (String s : list) {
    map.merge(s, 1, Integer::sum); // 如果没有,值设为1;否则,在原来的基础上加1
}

// map的值为一个列表。如果不存在,创建列表。
// key为字符串,value为储存字符串索引位置的列表
Map<String, List<Integer>> map = new HashMap<>();

for (int i = 0; i < list.size(); ++i) {
    map.computeIfAbsent(list.get(i), k -> new ArrayList<>()).add(i);
}

Bitwise & Bit Shift Operations

// Unary bitwise complement (~)
int n = ~5;  // ~0101 --> 1010

// Bitwise AND (&)
int m = 0b0001;
int result = n & m;   // 1010 -> 0000
// Bitwise inclusive OR (|)
result = n | m;   // 1011

// Bitwise exclusive OR (XOR) (^)
result = n ^ 0b1111;   // 1010 --> 0101

// Signed left shift (<<): shift once results integer x 2
result = 8 << 1;  // 16
result = -8 << 1; // -16

// Signed right shift (>>): shift to right and fill 0 on left, but keep leftmost bit.
// shift once results integer / 2
result = 8 >> 1;  // 4
result = -8 >> 1; // -4 

// Unsigned right shift (>>>): shift to right and fill 0 on left.
result = 8 >>> 1;  // 4
result = -8 >>> 1; // 2147483644

一些常用的用法:

// 乘或除2的n次方
int result = 3 << 4;  // 3 * (2 ^ 4)
result = 32 >> 3;     // 32 / (2 ^ 3)

// 判断奇偶
boolean isEven = (123 & 1) == 0;  // 注意由于优先级的原因,应使用括号

// 判断某位是否为1(比如每个位表示一种flag)
int mask = 0b0010;
boolean isSecondBitFromRightSet = (7 & mask) == mask;  // 7 = 0b0111, 7 & mask = 0b0010

// XOR:
// a XOR a = 0
// a XOR 0 = a
// a XOR b XOR c = a XOR (b XOR c)
// a XOR b XOR b = a

Trie

Trie又名“前缀树”,用于根据前缀查找词汇。典型的应用包括:自动完成、拼写检查、IP路由、解字谜等等。哈希表虽说查找的时间复杂度为O(1),但在查找相同前缀的词汇时就缺乏效率了。而且如果输入了大量的词汇的话,还有因哈希冲突而导致的效率下降。

如果输入的词汇数量为n,查找的前缀长度为m,那么查找的时间复杂度为O(m)。

这里是一个简单的实现:

public class Trie {
    public static class Node {
        private String word;
        private final Map<Character, Node> childrenMap = new HashMap<>();

        public Node() {
            // empty
        }

        public boolean hasChild(char c) {
            return childrenMap.containsKey(c);
        }

        public Node getChild(char c) {
            return childrenMap.get(c);
        }

        public Node addChild(char c) {
            return childrenMap.computeIfAbsent(c, k -> new Node());
        }

        public void setWord(String str) {
            this.word = str;
        }

        public String getWord() {
            return word;
        }
    }

    private final Node root = new Node();

    public Trie() {
        // empty
    }

    // 输入一个词汇
    public void insert(String word) {
        Node node = root;

        for (char c : word.toCharArray()) {
            node = node.addChild(c);
        }

        node.setWord(word);
    }

    // 查看是否有该词汇
    public boolean search(String word) {
        Node node = root;

        for (char c : word.toCharArray()) {
            node = node.getChild(c);

            if (node == null) {
                return false;
            }
        }

        return node.getWord() != null;
    }

    // 查看是否有以该前缀开始的词汇
    public boolean startsWith(String prefix) {
        Node node = root;

        for (char c : prefix.toCharArray()) {
            node = node.getChild(c);

            if (node == null) {
                return false;
            }
        }

        return true;
    }
}

如果词汇的组成字符是有限的(比如小写英文单词由26个字母组成),另一种实现方法是用一个数组代替Trie.Node中的HashMap。我个人不太喜欢这种方法。但是用Java语言的时候,讽刺的是这种方法往往能快一点点。

public static class Node {
    private static final int N = 26;    // 字符总数
    private Node[] links = new Node[N]; // 查找时用索引 int i = c - 'a';

    // 用flag代替上面的字符串能节省点空间。
    // 用字符串的目的是在需要得到词汇时不用从头遍历到目前结点,而是能直接拿到
    private boolean end = false;

    // ...

    private int getIndex(char c) {
        return c - 'a';
    }

    public boolean contains(char c) {
        return get(c) != null;
    }

    public Node get(char c) {
        return links[getIndex(c)];
    }

    // ...
}

 

 

深度优先遍历和广度优先遍历

深度优先遍历(DFS)和广度优先遍历(BFS)是两种遍历或搜索树和图的方法,常用于,呃,面试中……

深度优先遍历(DFS:Depth First Search)

DFS的遍历顺序又分为三种。下面以N代表当前结点,L代表左侧子结点,R代表右侧子结点。

  • 先序(preorder):先处理当前结点,然后对子结点从左向右处理。NLR,从上向下,从左向右
  • 中序(inorder):先处理左侧子结点,然后处理当前结点,最后处理右侧子结点。LNR,左–>当前–>右
  • 后序(postorder):先从左向右处理子结点,然后处理当前结点。LRN,从下向上,从左向右

如果把表达式放入二叉树,那么先序遍历的结果是前缀表示法(波兰表示法,Polish Notation),中序遍历的结果是中缀表示法,后序遍历的结果是后缀表示法(逆波兰表示法,Reverse Polish Notation)。

DFS的特点天然地适合使用递归的方法实现。三种顺序仅仅通过改变处理程序片段的位置就能实现,十分简单易行。用循环来实现的话,就得自己维护一个栈,而且实现起来也比较麻烦。

private void dfsInorder(Node node) {
    if (node == null) {
       return;
    }

    // 处理左子结点
    if (node.left != null) {
        dfsInorder(node.left);
    }

    // 处理本结点
    handle(node);

    // 处理右子结点
    if (ndoe.right != null) {
        dfsInorder(node.right);
    }
}

private void dfsPreorder(Node node) {
    if (node == null) {
       return;
    }

    // 处理本结点
    handle(node);

    // 处理子结点
    if (node.left != null) {
        dfsPreorder(node.left);
    }

    if (ndoe.right != null) {
        dfsPreorder(node.right);
    }
}

private void dfsPostorder(Node node) {
    if (node == null) {
       return;
    }

    // 处理子结点
    if (node.left != null) {
        dfsPostorder(node.left);
    }

    if (ndoe.right != null) {
        dfsPostorder(node.right);
    }

    // 处理本结点
    handle(node);
}

循环的方法:估计面试时不会要求做这个吧?其中,前序比较容易,而中序比较难,后序本来更难,但鉴于它与先序的对称性,可以把结果按先序(但是改为从右向左)压入栈中,再反向逐个弹出即可。

public void dfsPreorder(Node root) {
    if (root == null) {
        return;
    }

    Deque<Node> stack = new ArrayDeque<>();
    stack.push(root);

    while (!stack.isEmpty()) {
        Node node = stack.pop();
        handle(node);

        if (node.right!= null) {
            stack.push(node.right);
        }

        if (node.left != null) {
            stack.push(node.left);
        }
    }
}

public void dfsInorder(Node root) {
    if (root == null) {
        return;
    }

    Deque<Node> stack = new ArrayDeque<>();
    Node node = root;

    while (node != null || !stack.isEmpty()) {
        if (node != null) {
            stack.push(node);
            node = node.left;
        } else {
            node = stack.pop();
            handle(node);
            node = node.right;
        }
    }
}

public void dfsPostorder(Node root) {
    if (root == null) {
        return;
    }

    Deque<Node> stack = new ArrayDeque<>();
    Deque<Node> reversedStack = new ArrayDeque<>();
    stack.push(root);

    while (!stack.isEmpty()) {
        Node node = stack.pop();
        reversedStack.push(node);

        if (node.left != null) {
            stack.push(node.left);
        }

        if (node.right != null) {
            stack.push(node.right);
        }
    }

    while (!reversedStack.isEmpty()) {
        handle(reversedStack.pop());
    }
}

广度优先遍历(BFS:Breadth First Search)

BFS的访问顺序一般是从左向右,从上向下,一层一层地处理。一般用循环通过某种容器(queue、stack、set等)来实现。

public void bfs(Node root) {
    if (root == null) {
        return;
    }

    Queue<Node> queue = new LinkedList<>();
    queue.offer(root);

    while (!queue.isEmpty()) {
        Node node = queue.poll();
        handle(node);

        if (node.left != null) {
            queue.offer(node.left);
        }

        if (node.right != null) {
            queue.offer(node.right);
        }
    }
}

有时候在做BFS时,需要在处理完每一层后,都做一些其他的处理。这种情况下,可以在while循环中再套一个循环来实现:

public void bfsPerLevel(Node root) {
    if (root == null) {
        return;
    }

    Queue<Node> queue = new LinkedList<>();
    queue.offer(root);
    int count = 0;

    while (!queue.isEmpty()) {
        int n = queue.size();

        for (int i = 0; i < n; ++i) {
            Node node = queue.poll();
            handle(node);

            if (node.left != null) {
                queue.offer(node.left);
            }

            if (node.right != null) {
                queue.offer(node.right);
            }
        }

        // 此处加入处理完当前层后的代码
        log.info(">>> end of level {}", count++);
    }
}

一般来说,递归跟栈相关,所以对于DFS这种递归属性的算法,可以在循环中用栈来实现。而对于BFS这种跟栈没关系的算法,一般人就会认为不能用递归实现。但实际上还是有办法做到的:

private void recursiveBfsHelper(List<Node> nodeList) {
    if (nodeList.isEmpty()) {
        return;
    }

    List<Node> nextList = new ArrayList<>();

    for (Node node : nodeList) {
        handle(node);

        if (node.left != null) {
            nextList.add(node.left);
        }

        if (node.right != null) {
            nextList.add(node.right);
        }
    }

    recursiveBfsHelper(nextList);
}

public void recursiveBfs(Node root) {
    recursiveBfsHelper(Collections.singletonList(root));
}

DFS算法实现起来简短方便(用递归的话),因此十分适合在面试中使用。但有些问题则需要用到BFS,比如求迷宫最短路径问题(因为逐层扫描直到找到解为止,这个解一定是最优解(或最优解之一),这里的最优解是路径最短的意思。

例题:二叉树的序列化和反序列化

将二叉树序列化为一个字符串,并将字符串反序列化为二叉树。

  • 需要产生压缩的结果,否则对于只有右子树的二叉树,结果会过长。
  • 由于上述要求,用DFS比较恰当,因为产生的结果便于反序列化。
    // 序列化

    // DFS preorder
    private void dfs(TreeNode root, StringBuilder sb) {
        sb.append(",");

        if (root != null) {
            sb.append(root.val);
            dfs(root.left, sb);
            dfs(root.right, sb);
        }
    }

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null) {
            return "";
        }

        StringBuilder sb = new StringBuilder(root.val);
        dfs(root.left, sb);
        dfs(root.right, sb);

        return sb.toString();
    }

    // 反序列化

    private TreeNode dfs(Queue<String> queue) {
        TreeNode root = null;

        if (!queue.isEmpty()) {
            String s = queue.poll();

            if (!s.equals("")) {
                root = new TreeNode(Integer.parseInt(s));
                root.left = dfs(queue);
                root.right = dfs(queue);
            }
        }

        return root;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data == null || data.length() == 0) {
            return null;
        }

        Queue<String> queue = Arrays.stream(data.split(",", -1)
                .collect(Collectors.toCollection(LinkedList::new));

        return dfs(queue);
    }

7年前的苹果笔记本的升级和更新

我的苹果笔记本是大约7年前买的,型号466,是第一代铝壳MacBook,苹果公司一般称之为“MacBook (13 英寸, 铝壳, 2008 年末机型)”。我当时安装Windows XP时,从总容量160GB的硬盘里,分配了30GB的硬盘空间。结果一路用下来,基本上全是用Windows,而极少启动到苹果的OS X。用到后来,Windows的剩余硬盘空间便经常吃紧,剩余硬盘空间经常维持在几百MB的程度。因此,感觉十分别扭。

当年买苹果笔记本的原因主要是经济层面上的,当时像点样子的笔记本电脑都要比苹果的这款产品贵不少,而且品质还差,加之苹果笔记本已经开始用英特尔的CPU了,能装Windows,因此当然要买这款了。后来基本上没启动到苹果的操作系统的原因,一方面是对OS X 10.5的操作有些不满,比如无法把窗口最大化,小小的窗口用起来很别扭;另一方面是后来很多应用程序都只支持10.6以后的版本。

几个星期前,我终于忍无可忍,就试图再分配一些到Windows侧。上网了解了一些方法,比如先到苹果一侧把分区改小,再到Windows侧把分区扩大。苹果侧倒是没问题,而且扩大Windows分区时,我还有所保留,没有用全部剩余空间(因为据我观察,苹果分区和Windows分区之间应该有大约100MB的剩余空间),结果从Windows切换到苹果后,就再也无法启动Windows了,因为苹果侧对增大了的Windows分区并不认同!又由于我只有这一块硬盘,因此也无法用其他软件去修改分区。好在我当时已经把重要文件从Windows侧备份到我的服务器里了。之后一段时间,我就没有电脑可用了,只能在服务器里勉强装一个虚拟的Windows电脑。当然,其性能很差了。

此时,我就面临一个选择:买一台新的笔记本电脑,或修好这台电脑。新的Windows笔记本电脑还是比较贵,而且,我也难以认同Windows 8的那种风格(Windows 10似乎有所改观,但还是摆脱不了Windows 8的调调)。而新型的苹果笔记本,虽说其“视网膜显示屏”很精致,而且也更薄更轻,但是,内存却是焊死在主板上的,要更多内存就要付出更大的不成比例的代价。这让我感觉十分不爽。于是,我就决定以较低的成本来升级和更新目前这台电脑。

由于IT技术的飞速发展,一般来说,每3年就该换一台电脑(如果不是只用来上网娱乐的话)。但我作为一名软件工程师,在买电脑时,都是选择配置相对较好的,因此,也能多用一段时间。

因为事情的起因在于硬盘,因此,首先要买一块容量更大的新硬盘。这里又有两个选择:买速度极快且防震的SSD硬盘,还是买更便宜的普通硬盘?我本来想买SSD硬盘,因为时间就是金钱、就是生命。但是,SSD硬盘有寿命问题。比如在Windows XP里,我们能看到,系统几乎时时刻刻要写日志文件。如果总在一个地方写,难免要把硬盘很快写坏(XP里没有对SSD硬盘写操作的分散的功能)。而这种不可靠性是我所无法接受的。于是,就决定买普通硬盘了,而且用原计划一半以下的钱买了两倍的容量:买了目前性价比最好的1TB的硬盘,7200转的,速度也比原装的(5400转)快。

CPU是焊死在主板上的,无法升级。接下来就是内存了。买这台电脑时,已经装了官方声称的最大容量:4GB内存。不过,据说这台电脑实际上能支持8GB内存,条件是用比较新的苹果操作系统,并升级固件。固件在买来后不久就升级了,于是,就买了8GB的内存。买的时候要注意型号和规格,不要到时候装上用不了。

当然,还要更新苹果的操作系统。一个好消息是,只要有OS X10.6(雪豹)或以上的版本,就能免费更新到最新的版本。于是,到苹果的网店买了一份几年前的雪豹,花费2400日元。

mac-osx-snow-leopard

但是,笔记本内置的光驱坏了!我一直不看好这种吸入式光驱,因为其机械结构更复杂,更容易坏。我拆开电脑,取出光驱,结果对如何修好竟然毫无头绪!我完全没看出该如何修理,网上也没有这样的内容。有人问苹果公司,答复是换新的。光驱本身明明很好,只是吸入装置坏了,于是就要重新换价值不菲的光驱,这是我所无法接受的。苹果电脑还能借用其他电脑的光驱,因为一些新的型号由于更薄,都没装有光驱。但我试图连接我在服务器里虚拟的电脑的虚拟光驱时,竟然无法连接!(在旧系统基础上升级还是可以的)于是,就需要再买一个光驱。网上卖的一些光驱很漂亮,很有苹果风格,但是都是吸入式的。吸入式光驱的确漂亮,但是,在可以自由选择的情况下,我当然不会再自找麻烦,于是就买了一个比较便宜的普通的光驱。这款光驱除了还算轻薄规整外,说实话做工的确算不上精良,跟我在1998年买的第一台索尼笔记本电脑配的CD光驱比差了不少,但胜在便宜。当然,买光驱时也要注意是否对应苹果电脑。虽说光驱一般没什么问题,可是厂家明确声称支持苹果电脑的话,还是会更加放心。

万事俱备,于是就开始安装硬件了。先打开电脑,把原来的硬盘卸下来,把上面的四个螺丝拧下,装在新硬盘上。这四个螺丝钉的作用是把硬盘支撑和固定在支架上。为方便起见,把原来的硬盘上的塑料带也撕下来,贴到新硬盘上。这条塑料带的唯一用处是为了便于把硬盘拆下来……此时,就可以把新硬盘装入电脑了。

所用工具

所用工具

old-hdd

卸下原装的硬盘,拧下4个螺丝(用于支撑和固定硬盘),撕下塑料带

new-hdd

把支撑螺丝钉拧到新硬盘上

old-new-hdd

把塑料带贴到新硬盘上

install-new-hdd

安装新硬盘并固定

然后,连上外接光驱,安装雪豹版操作系统。感觉比豹子版是好用一些了。但不能留恋,直接一步到位,升级到最新的OS X EI Capitan!果然漂亮多了,而且,据我之前到店里的了解,新版也比较容易最大化了,双击缺省的行为也不是最小化了。于是,操作性比较符合我的心愿了。

雪豹版安装盘

雪豹版安装盘印刷精美可爱,可收藏

关机并再次把笔记本后盖打开,把原来的2GB的两条内存卸下,换上两条新的4GB内存。

dvd-mem

安装光盘和两条4GB内存

重新启动系统,可以确认内存升级到8GB。

mem-8gb

内存成功升级到8GB!

不管怎么说,还是要安装一下Windows XP。这次吸取了上次的教训,分配了400GB给Windows,其余600GB留在苹果这里。用系统带的Boot Camp助理无法装XP,于是就用“磁盘工具”进行分区,然后直接用Windows XP的光盘重新启动并安装。安装完毕后,再安装雪豹带的驱动程序,于是顺利完成。(注:这里应该有更简单的方法:装完雪豹后,直接用其Boot Camp助理分区并安装Windows XP,然后启动到Windows装驱动程序,然后再回到Mac并升级到EI Capitan,最后升级内存。我当时急着装内存,就跳过装Windows XP了。)

我买的Office 2007是有安装数量限制的,本来已经没有剩余了,但这次居然能顺利激活。说明它还是认识这台电脑的硬件的,尽管内存和硬盘都换了。

但是,Windows XP却无法更新!因为微软早就放弃对XP的支持了。好在微软还是继续对XP的POS机系统提供支持,于是,就修改一下注册表,把自己伪装成POS机,来更新系统。但是又遇到新问题!XP里原装的IE6版本太低,无法正确装载更新网站!于是,先安装Firefox,再到微软网站搜索SP3和IE8的安装程序,并安装之。这样,IE终于符合微软更新网站的条件了。接下来就是分期分批地安装数不清的更新补丁。费了几天时间(下班后回家处理),终于装完了全部更新。但是,MS Live却不能再安装,无法用其Email客户端了。费了一点时间到网上搜索,终于在Outlook里把Hotmail账户都设置好了。

Windows XP的32位版本无法使用所安装的全部8GB内存。好像只能使用3GB左右,开启PAE也无济于事。于是,就安装了之前购买的Buffalo MiniStation外接硬盘里所附带的REMDISK软件(以前下载的免费软件最大只能支持3GB,对现在的情况来说已经不够用了),把剩余的5GB左右的内存变成了一个虚拟的异常高速的硬盘。然后,修改Windows的设置,把临时文件夹和pagefile都指向该虚拟盘。这样,就不但能使用3GB以上的内存,而且还避免了把一些垃圾(临时文件)写到硬盘里。另外还要做以下两件事来把事情处理完美:

  • 在Autoexec.bat(恐怕有20多年没跟它打交道了)里加一条命令,在系统启动后,在虚拟盘里创建“temp”文件夹
  • 修改注册表,在Windows关机时清空pagefile文件(防止再次启动时,系统需要从pagefile里读一些数据)

设置完以后,突然觉得Windows XP有些简陋和过时了,心里有点落寞的感觉。可能主要是由于使用了苹果的最新的系统的原因吧。恐怕以后多数时候要改用苹果的系统了。

回到苹果侧,安装了X Code和Eclipse等开发工具,并设置好电子邮件账户。

最近,又买了一条亚马逊做的苹果笔记本用的廉价版显示器连线,配合之前买的一个比较便宜的切换器,就能在该笔记本以及另外两台Linux服务器间共享液晶显示器、键盘和鼠标了。笔记本用的罗技的蓝牙无线鼠标(M555b)倒是目前最好的一个,5年来一直在用;但键盘当然还是我的HHKB好,尽管在苹果机里面一些快捷键比较难以使用……这一点以后慢慢摸索和解决吧(主要原因是其驱动程序尚未支持EI Capitan)(后记:现已支持)。

   

另外,连好后,我发现显示器的显示质量比预想中的差,在Windows XP中只能设为1024×768!于是到苹果侧,经过一番折腾,固定在1280×1024,因为经过上网搜索,其native的规格就是这样。毕竟是十多年前买的显示器了,型号是三星的SyncMaster 172T。当年很好也很贵的东西,经过了这么多年,当然是技术落后了。上网一看,当年买这台17英寸显示器的钱,现在买两台普通的27英寸显示器都还有富裕。看来是由于多年来缺乏台式机,而服务器的显示卡又差,并参照公司里用的显示器,于是便对它有一些不切实际的过高期待……

下一步的计划是,在用一段时间后,如果内置光驱果真是无药可救了的话,就把它去掉,并买一个装硬盘用的适配器,把原来那块硬盘装进去,并把其中的Windows分区修好。这样,就有两个硬盘可用了,而且,以后如果需要,还可以把那个容量小的硬盘升级成2TB或以上的硬盘,或者换成SSD硬盘。

本次更新总共开销情况(价格为购买时的价格):

商品 价格(日元)
硬盘 7,299
内存 6,250
光驱 2,680
苹果雪豹 2,400
小计 18,629
切换器 2,300
MiniDisplay到VGA变换连线 1,480
合计 22,409

关于自己现有的计算机设备的思考

前段时间,惠普塔式服务器出现故障,于是切换到更小巧更节能更静音的惠普微型服务器N54L
。虽说它的显示卡更好,分辨率更高,不像以前的服务器那么粗糙,但是它的性能却是有不少的下降。当然,该服务器原本就是为了当文件服务器用的,所以CPU和内存都不太出色,而且还没有光驱!其实,打开机盖,可见当初设计时是为光驱预留了空间的,上面还有一个电源接口。可是,我费了好大力气把它拆开(因为微型,所以内部设计过于紧凑),结果发现没有多余的插槽来接数据线!我本想把原来服务器上的光驱装上的!这样,如果没有USB外置光驱的话(我只有一个15年前买的SonyCD-ROM光驱),当然是不够用的。现在装Linux都要DVD才行。当然,现在要买一个USB的外置DVD可写光驱也很便宜了,只需3000日元左右。另一个方案是用USB内存卡装。我有一个古老的1GB的内存卡,居然无法制作可启动盘,原因是无法格式化为FAT32格式,只能格式化为FAT格式,不知到为什么。

不论如何,以前曾装过Linux,虽说不是最新版,但只有先凑合用着。又费了些力气,把它设置好,并把原来的网站和数据库移植了过来。终于又顺利上线了。

之后,又在里面虚拟了一个Windows XP机器,打算做台式机用。因为现在用的苹果笔记本里装的Windows XP系统,已经“盘满为患”。所以,服务器下线也影响到外部存储。但是,虚拟机性能太差,用作台式机的话实在难受,比我十多年前买的台式机都差,于是只好先放在那里。

后来又想到用Linux服务器兼作台式机,至少上上网、收发一下电子邮件总还行吧?可能的话,还可以用于软件开发。结果上网还算勉强可以,但别指望看视频(YouTube等),速度实在不行,而且还没装声卡;收发Email更是慢得令人难以忍受。于是,只好继续用现在的苹果笔记本。又费了点时间把Samba服务器再次设置好,可以负担一部分存储的任务,让笔记本硬盘的存储压力得以减轻。

后来发现现有的Linux版本有很多限制。而如果擅自把PHP、MySQL等升级的话,又会使得现有网站程序无法正常运行!看来PHP的版本兼容性实在太差。以前也曾经有好几次升级后不得不对很多网站程序到处修修补补,这次实在暂时无此精力,于是只好先这样用着。于是想到了之前发生故障等那台塔式服务器,仔细检视了一下主板,发现似乎并无大碍。 我原以为之前的硬盘出现了问题,换一个原来的硬盘(之前用的是最初的HP塔式服务器的硬盘直接接进去的),经过尝试用USB内存卡安装无效后,由于设置好了Samba,在服务器里下载了新版Linux的安装盘镜像文件,再通过Samba在笔记本里刻录成DVD光盘,在塔式服务器里安装。结果光盘安装程序启动失败!

于是对硬件检查里一下,主要是测试了一下内存,发现内存有大量问题!把内存都拆下来,再装入一个2GB的内存条,检测正常!再装一个2GB内存条,结果还是正常。看来是另外两条512MB内存有问题。想不到内存居然也会出问题,可能是因长时间运行产生的过热所致吧?

于是再次开始安装。这次终于顺利安装好了。虽说有很多问题尚待解决,比如不能上网(!),但至少一切都正常了,而且恢复了一台功能较强大的服务器(显示卡差点),可以用来在新版Linux下调试网站程序,修复不兼容问题。而且试了一下新版操作系统,居然能认得我的佳能打印机,这样就有望用做打印服务器。

但是,我还是计划继续使用现在的微服务器,因为它又省电又静音。塔式服务器可以在需要时开机。

服务器的现状

  • 网站服务器
  • 文件服务器
  • Linux开发环境(偶尔会用到)

不成功的功能:

  • 虚拟机(Windows XP系统)
  • 台式机(Linux系统)

以后计划要做的:

  • 把操作系统升级到最新版本
  • 打印服务器
  • 私用的云服务器(用于跟iPhone等设备连接,并存储一些文件,可在各个设备上同步)
  • 流媒体服务器(用于存储和共享一些关于孩子的视频)
  • VPN服务器(用于在海外旅游时,从手机访问一些难以在当地访问的网站)

现有其他硬件设备

苹果笔记本电脑:2009年年初买的,用了6年半了,现在性能还是够用。只是几年前原厂的电池鼓了,在网上买了一个较便宜的电池(可能是代工工厂流出的),但目前在外面只能用1个小时左右,换言之,没有电源的话,基本上无法在外面使用。120GB的硬盘,分给Windows系统30GB,也基本满了。

对策:

  • 买个新的笔记本电脑:新的苹果笔记本看上去很好:更轻更薄;电池续航能力也有10个小时左右;显示屏更精细;硬盘也换成了内存式的SSD固态硬盘,存取更加快速;各接口也更先进;操作系统也更好。但CPU似乎没有显著的提高,没有光驱,而且内存条做死在主板上,无法自己扩充。内存问题应该是苹果的赚钱策略,但让人有点不爽。
  • 自己换电池,并换一个内存式SSD固态硬盘:目前的SSD硬盘还是有点贵,而且还要把原来的内容都弄到新硬盘上,因为这些麻烦而有点不太情愿。
  • 把苹果笔记本主要当台式机用,在外面使用iPhone:现在在外面主要用iPhone 5s。iPhone的很多程序其实比计算机版更好用,比如一些网上银行应用等金融、理财、投资方面的应用。缺点除了计算能力差(毕竟不是真正的计算机),无法用于软件开发等任务外,主要有两个弱点:屏幕太小,以及键盘更小。屏幕的问题是不容易解决的,虽说换一个iPhone 6s Plus的话能在一定程度上解决,但并非根本性的解决。可是由于程序更好用,用习惯了也不错。键盘的问题却是致命性的,如果写一封较长的Email,要花很大精力。我最近买了一个Logicool的小巧的蓝牙键盘Logicool iK1041,基本上解决了这一问题。又轻又薄,而且具有防水功能,在咖啡馆里把咖啡打翻在上面也不怕。虽说携带型差些,无法装入衣服口袋里,但要写一篇长文章的话,真的很好用。我这篇文章就是用该键盘在iPhone上写的。

iPad Pro还是MacBook?

苹果的新产品发布会上介绍的iPad Pro的确比较吸引人:它不但有匹敌甚至超过笔记本电脑的计算能力,而且更能配合Apple Pencil使用,能手写输入,可以快速记录,或画草图,更可以用来进行书法和绘画的创作,因为它不但毫无时滞,而且还能表现出不同粗细的笔触。同时,它能同时在一个屏幕上显示两个程序,可以一边参照资料,一边工作。还有画中画的功能,在工作时开着新闻视频或在线视频聊天窗口,一心二用。然而,尽管可以用新的附属键盘把它打扮成笔记本的样子,但受制于其iOS操作系统,它还是不是真正的计算机,对于笔记本电脑的一些十分基本的多窗口功能,它还是无法实现。这多少有点让其超强的硬件显得可惜了。

MacBook则是功能强大的笔记本电脑,同时跟以前的版本相比,又更轻更薄,在携带型上并不亚于iPad。当然,iPad能拿出来就用,而笔记本电脑则不得不先在桌上放下,打开机盖,然后才能使用。虽说听上去并没什么,但在某些情况下,这点小差别还是很重要的。而且也不能在屏幕上手写输入。这主要是对快速记录和艺术创作有影响。对于艺术家来说,就算接上了手写板,也总比不上直接在屏幕上”所见即所得”地创作吧?

对于我来说,目前基本没有艺术创作的需要,因为我缺乏这方面的才能。不过,我曾经想学习书写梵语,而梵语由于在佛教中被看得很神圣,写完了是不可以随意丢弃或放到不恰当的地方的。在这种情况下,当然是在电子设备上书写更方便,对于不再需要的作品,可以简单地删除掉。因此,在一些特定的情况下,对我这种非艺术家的用户也是很有用的。

我的职业由于是程序员,因此,笔记本电脑的软件开发能力对我来说就更重要。因此,如果二者选其一的话,我会选MacBook,之后有必要时再添一个iPad Pro。

iPhone 6s Plus?

我其实并不喜欢那么大的屏幕。现有的iPhone 5s携带性很好,也不会因为放在裤子的后兜里坐下而把它折弯,但屏幕还是有点小,尤其是用于阅读和写作时。iPhone 6s可能大小比较合适,但追求屏幕的实用性的话,也许就应该牺牲一些携带性了。还有就是iPhone 6s Plus具有iPhone 6s所没有的拍摄防抖功能。

我目前并没有换手机的紧迫性,但现有手机是有SIM锁定的,只能用Docomo的昂贵服务,而不能用不断兴起的廉价手机服务(严格说,其实也能使用租用Docomo网络的分销商的廉价手机服务),也无法在海外用其他的电信公司的卡。因此,以后可能还是要因为上述各种理由而更换手机。在这种情况下,可能就是iPhone 6s Plus了吧?

台式机?

如果能像苹果电视那样十分小巧便携,而且又不贵的话,倒是可以考虑。但现在也没有紧迫性。强调便携性也是为了方便地带到其他地方使用。

现在其实有一些这类产品,包括苹果自己也有。但尺寸小了,性能未免要有所下降。这样,就不如继续使用笔记本电脑了。因此,目前来看,只要有笔记本电脑倒是也足够了。

程序员的职业前景会比较好

读过一篇文章,说近来美国景气在不断回升,就业人数也不断增加,但就业人口有一个明显特征,就是社区大学(相当于日本的“短期大学”,也许相当于国内的大专?)毕业的比例越来越少。从具体的职业角度来看,事务性职务正在逐渐被计算机所取代。

随着美国的系统化进程的不断进展,事务性职员所从事的进度(schedule)管理、票据整理等工作正逐步被计算机越来越多地取代。而且,不只是这类低端工作,就是如会计、律师、医师、分析师和客机飞行员等传统的高端工作,也正在逐渐地被计算机系统侵蚀。众所周知,产业机器人以其精准、迅捷、能耐受长时间劳动以及绝对服从命令的特点正逐步取代产业工人的职业;会计系统已经存在了好长时间;一些比较标准的法律事务,也可以用计算机系统代替;飞机的起飞、巡航和降落都可以由电脑自动完成,也许主要需要飞行员的地方,就是驾驶飞机从登机口走到跑道上去,以及降落后从跑道回到到达出口?医疗方面,由于各种病症都有一些典型特征,医疗数据库正逐步完备,因此,计算机疾病诊断系统正在发挥越来越大的作用。IBM的人工智能系统“华生”,具有高级自然语言处理、信息检索、知识表示、自动推理、机器学习等功能,曾在智力问答电视节目中击败过两位冠军,后从事金融分析工作,偶尔还化身为主厨开发出营养、美味的菜单。近来,IBM正在利用华生无与伦比的大数据处理能力,进军医疗产业。这也将是华生的主战场和最终目标(而且这也更贴合其“华生”的名字)。

因此,在这样一个越来越自动化和智能化的时代,我们人类该何去何从就成了一个越来越紧要的问题。我觉得,自己所从事的程序员(软件工程师)这一职业,倒是会有比较好的前景,因为我们可以选择站在这一系列的事物的制高点上。因此,要学好编程的核心技术,并在此基础上学习一些人工智能、机器学习等方面的知识。

目前我供职的公司是一家比较传统化的日本企业,很多手续还需要在纸上盖章。最近开发了一个系统,在部署时,有一些令人啼笑皆非的情况。该系统分析、整理客户的交易数据,并发送到分析平台去。公司当然希望尽可能快地提供交易报告给客户,虽说不必如交易一般分秒必争,可也是越快越好,因为这关系着客户体验和公司形象。我把系统从最初需要数日(因为作为数据源的数据库有很多问题,且没有优化!)的处理时间,通过运用多线程处理、内存数据库等多种技术逐步缩短到10分钟以内。在我们讨论是否应该宁可先少包括一些数据也要提前10分钟执行数据处理时,负责部署的一方(公司的子公司)提出,在出现意外时,处理流程应该是这样的:数据中心发现错误;数据中心打电话给负责部署和支持的团队;负责支持的团队初步判断一下情况,并打电话通知我(或其他成员);我要求系统记录的处理日志和生成的数据文件;支持团队把请求通知给数据中心;数据中心提供这些文件给支持团队;支持团队把文件发送过来……由于人不可能总在自己的位子上不动,可能还需要做其他工作,或开会,甚至是上厕所等,因此,每一步都可能发生10分钟甚至半个小时以上的延迟。这样下来,真正传达到我这里就不知道是什么时候了。我的设计是在系统处理完后,立即检验结果,并把检验报告通过电子邮件发到我所在的团队。可是,数据中心不能发电子邮件!不知道他们这样做是真的出于(毫无必要的)安全性的考虑,还是为了让这一大群人有工作可做?这显然是不能接受的,好在数据中心可以拷贝文件,于是设计改为:在系统处理完成后,把相关的所有文件都拷贝到另一台由我们所控制的服务器;我在服务器上运行一个监控进程,发现这些文件后,立即进行检验并把检验结果通过电子邮件发给我所在的团队。这样,就不用为了这点事每天疲于奔命地折腾了。也许像我这样用软件来代替人工有点冷酷(被代替的也包括我自己的一部分),可是不这样的话,不仅是毫无必要地浪费公司的经费,而且更是对生命的极大浪费!

因此,我们要做的不是苟且地通过做这些没完没了的琐碎工作而混日子,而是要不断地提高自己的能力,做更有创造性的工作。如果有一天绝大多数的工作都被电脑所取代,人类会何去何从?如果财务自由了,到时候会不会有新的财务危机?这些就不是我能想清楚的问题了。但人生如梦,本虚幻不实;时光如电,我们当“如救头燃”般地珍惜时间。因此,到那个时候会如何本身并不重要,重要的是现在如何。子曰:“朝闻道,夕死可矣。”就不必为未来的世间琐事而烦恼了。

我的BASIC语言解释器(My BASIC Interpreter)

这一段时间由于要找工作,因此,如果幸运地获得面试的机会的话(现在多么不容易啊),作为一名软件工程师,我免不了要被问大量的技术性问题。但由于现在不工作,平时不用的话,这些知识很容易被大脑驱逐出主要存储区域,而转到类似“SWAP”的地方。遗憾的是,大脑不比电脑,从“SWAP”里调东西不只是慢一些的问题,而是需要极其复杂的手续,比如催眠、打坐冥想、灵机一动、状态极佳等等,都是在面试时不太现实的。因此,必须多多使用这些知识,让大脑认为它们很重要,从而调到主要的快速存储区域里去。这样才有望在面试中对答如流。

但是,作为超级现实主义的本人来说,弄一些没用的东西是对时间和生命的极大浪费。因此,我就给自己设计了一个有用的项目:做一个BASIC语言解释器,将来可以加入很多算法、统计学、金融相关的计算功能,作为一种“内嵌语言”工具来使用。经过一段时间的努力,终于初步成型。目前还达不到1.0版的要求,但已经能正常执行很多程序,并能评价各种表达式的值。

另,还有一个做该项目的原因:我在之前的工作中用到某大公司的一个系统,他们提供TCL作为可编程的借口和内嵌语言。我的工作模式是:编写各种计算程序,并将各种功能做成TCL命令,跟TCL系统结合起来。然后,就是用TCL编写脚本程序来实现公司的各种计算需求。但TCL语言是一种功能很弱的语言,不但无法评价表达式的值(必须用expr命令来评价,十分麻烦),而且其各种流程控制结构也很笨拙。其他常用的语言,比如Python,我不喜欢它对程序书写格式的限制(比如每行前面要加多少个制表符等),把它跟C++或Java语言程序集成在一起也很令人头疼;而JavaScript(Rhino)我也不喜欢。因此,我就想做一个更好的,以便将来有用。选择BASIC语言的原因,一方面是BASIC十分简单易用,而又提供足够的威力来控制流程和进行计算;另一方面,BASIC语言是我在多年前(高中时)学的第一种编程语言,可能跟比尔·盖茨似的,对它有某种感情吧。

目前的版本大约达到0.6版的程度了吧?主要功能都实现了,但还没有支持数组(多维)、日期和时间计算以及将来要加入的各种算法、统计计算和金融计算等。目前的状态也不是一个嵌入式语言,而是一个命令行BASIC解释器或命令行表达式评价程序的形式。该程序会把结果输出到stdout(标准输出设备),并返回0。如果有错误发生,则向stderr(标准错误设备)输出错误信息,并返回1。本软件为非开源的免费软件,可免费使用和传播,但禁止把它单独或作为系统的一部分销售给他人。可以在这里下载。

由于本来的目的是把它作为嵌入式语言工具使用,因此,只保留了BASIC语言中一些主要的功能(精华?),把那些陈芝麻烂谷子的糟粕通通舍弃了。同时,又从Visual Basic里借来一些新的特性(比如:函数的参数按引用传递、用RETURN返回、循环体内的CONTINUE等)。

本软件用C++(以及STL)编写,没用其他第三方软件(比如yacc等)。

该软件的使用方法:

mybasic -e <表达式>

或者

mybasic -f <BASIC程序的文件名>

演示:

my-basic-demo

演示的程序(test.bas):

function mySum (byval x as integer) as integer
	if x <= 1 then
		return x
	else
		return x + mySum(x - 1)
	end if
end function

print mysum(100)

本程序的规格说明:

表达式评价

表达式以BASIC语言的语法书写。该程序支持算术、字符串、关系和逻辑运算。

数据类型

  • boolean
  • integer
  • double
  • string

运算符

  • 数学运算符: ^, +(一元运算符), -(一元运算符), *, /, \, MOD, +, –
  • 字符串运算符: +
  • 关系运算符: =, <>, <, <=, >, >=
  • 逻辑运算符: NOT, AND, OR
  • 其他: (, ) (圆括号,为了改变运算符的优先级)

数学函数

abs, sign, int, sqrt, exp, log, log10, rad(角度变弧度), deg(弧度变角度), sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh, sec, csc, pi(返回圆周率), random(返回[0, 1]区间内的随机数)

字符串函数

str(num)(数字变字符串), space(n), tab(n), ltrim(str), rtrim(str), trim(str), len(str), ucase(str), lcase(str), val(str)(字符串变数字), isNumeric(str), left(str, n), right(str, n), mid(str, from, n), instr(str1, str2)(如果str1中含有str2,则返回从0开始的索引;如果没有,则返回-1。)

与BASIC语言不同的地方

  • 一些函数的函数名不同。比如,不是“sqr”,而是“sqrt”。
  • 字符串的索引从0开始,而不是从1开始。
  • 不支持BASIC语言的类型字符(比如,“2#”表示double型的2),因为这些实在令人讨厌。。。

BASIC 解释器

本BASIC解释器支持一个实用简练的 BASIC 语法。

“实用简练的 BASIC 语法”的规格(除了上述的表达式规格以外):

支持的特性

  • 注释:REM, ‘(单引号)
  • 变量声明:DIM … AS …
  • 条件语句(块IF):IF, THEN, ELSEIF, ELSE, END IF
  • FOR循环:FOR, TO, STEP, NEXT, CONTINUE FOR, EXIT FOR
  • DO循环:DO, WHILE, UNTIL, LOOP, CONTINUE DO, EXIT DO
  • 函数和过程:FUNCTION, END FUNCTION, SUB, END SUB, RETURN, BYVAL, BYREF
  • 输出:PRINT
  • 其他:END

不支持的特性

  • LET(最没用的命令。直接无视之。)
  • 行号(太古老陈旧)
  • GOTO(万恶之源)
  • GUSUB…RETURN(太古老陈旧)
  • DEF FN(太古老陈旧)
  • 标签(label)(既然GOTO、GOSUB等都舍弃掉了,也就没有必要使用标签了)
  • 单行的IF
  • READ…DATA(太古老陈旧)
  • SELECT CASE(不喜欢该语法。但又不想加入如C++的switch/case等太不一样的东西。可以用IF代替。)
  • ON [ERROR] …(太古老陈旧)
  • WHILE … WEND(太古老陈旧,应以更强大的 DO … LOOP 代替)
  • EXIT SUB, EXIT FUNCTION(应以 RETURN 代替)
  • CALL(函数有返回类型凭什么就不能像过程那样直接调用?用CALL太麻烦,去掉。)
  • variant 数据类型(降低代码质量)
  • 用户定义类型(比如“类”等,因为实现太麻烦,而且在短小的脚本编程中并不需要。你要写较长的脚本?那为什么不选择一种严肃的语言呢?比如Java、C++等。)
  • PRINT USING(如果必要,可考虑将来加进去)
  • 文件 I/O(OPEN 等。在以后的版本中会加入新的文件I/O函数)

与一般的 BASIC 的不同之处

  • 变量必须声明(用DIM)之后才能使用(相当于在 VB 中总是声明“OPTIONAL EXPLICIT”。为了提高代码质量。)
  • “DIM a, b, c AS DOUBLE”可以,但不支持“DIM a AS INTEGER, b AS DOUBLE”(为了提高代码质量,使程序易读)
  • Boolean型和Integer型不能相互转换(为了提高代码质量)
  • 函数或过程:必须声明 BYVAL 或 BYREF(没有缺省。为了提高代码质量。)
  • PRINT 语句中:
    • “,”意思是插入制表符,而不是从某一列开始打印(原来的规格太古老陈旧,已不适用于今天的使用环境)
    • “;”意思是紧接着上次的地方打印。这跟原规格相同,但不会在数字前自动插入一个空格(必要时,程序员应该负责做这项工作)

尚未支持的特性(会在将来的版本中支持)

  • 数据类型
    • 数组(多维)
      • 数组的下标总是从0开始
      • 在“DIM A(N) AS INTEGER”语句中,N 是指数组的大小,不是最大下标(最大下标为 N – 1)
    • date
    • time
    • datetime
  • 内置函数
    • 一些统计类函数
    • 一些数值算法
    • 文件 I/O
    • 其他的互操作性(?)
  • 命令
    • INPUT (?)

更换了服务器,及由此想到的

俗话说,“倒霉的时候喝凉水都塞牙”。刚发布完上一篇文章,想到要更新一下系统,就更新了一下并重新启动一次。上次重新启动是在大约半年前,当时我刚刚回到国内,Web服务器就突然不工作了。系统重启后顺利解决。之后就一直没有重启过系统。这次重启后,过了一段时间,服务器突然再次自己重启!而且,从log里找不到任何蛛丝马迹。想到现在天热,而且风扇和散热片很长时间没清理了,就决定趁此机会彻底清理一下。于是,清洗散热片,清洗风扇叶片,清洗服务器前面板,重新涂导热胶,并把这些一一装回。再次启动系统。结果第二天,我发现服务器居然是停着的!启动时,我注意到一条错误信息,说保存的记录已满。于是,赶紧到BIOS里看了一下,结果让我很惊讶:里面充满了错误记录,说CPU的电压不稳。其表现是电压突然增高,然后1秒后又恢复正常,不断地重复着这种过程。再次启动后,也是过一段时间就死机。看来麻烦大了。。。

开始,我一直以为是我做错了什么导致的,因为在我更新系统以及清洗硬件之前,系统一直持续工作。为什么偏偏在我做完这些后出问题了呢?我起初怀疑系统更新有问题,就将几个可疑的程序退回原来的版本。这样还不行,我就怀疑硬件的问题了:是我取下CPU时做错了什么吗?再次取下,发现几个触点处有点污渍,于是将其擦掉。然后是导热胶没布满CPU后盖(商品说明上让加一个米粒大小,我已经加了黄豆大小了。。。),但回忆起原来是涂满的,于是,再涂了一些,直至涂满。但问题依旧。于是,取来明亮的LED照明设备,仔细观察了一下主板上各处,终于发现有两个电容器外表不正常,有点鼓了。由于主板主要是用电容器来稳压的,看来这就是问题真正的原因了。想到它从2007年初购买回来一直24×7地持续工作到现在,不禁让我心里有一种悲戚的感觉。

事已至此,看来该服务器是难以简单地修好了,除非DIY一下把那两个电容器换了。但与此相比,还有更便捷的方法:在2010年时,通过服务器赚了一点点钱,但这台服务器死慢,于是,就进行了一项小小的设备投资,用了旧服务器一般的价格从网上拍来一台下一代的服务器。当时也是觉得一台服务器风险太大,有个备份总是好的。但鉴于服务器噪音大,耗电量多,而且系统升级后原服务器的速度突然提高了不少(看来是原来的Linux Distribution有问题),加之在新服务器上装系统后居然连不上网,由于当时比较忙,就把它搁置在一边了。这次拿出来,检查了一下,上不了网的原因居然是DNS没设置好的缘故(系统安装后上不了网的确比较荒谬)。于是,索性下载了下一个版本的操作系统,把这台服务器初步设置好了。由于这台服务器有4核的CPU(原来的为双核),而且原来的1GB内存也用旧服务器上的5GB内存换掉了,因此,系统资源比较充足。显示分辨率也由原来的1024×768提高到1280×1024。没办法,服务器的显示就是这么差(也是为了把好钢用在刀刃上吧)。可惜了我的显示器了。由于系统资源比较足,为了充分利用,就又在里面虚拟了一台Windows XP的虚拟机出来,速度还不错。我已经很长时间没有自己的台式机了。。。

之后就是恢复各个网站。于是从旧服务器里拆下硬盘,装到新服务器里。我突然想,是不是先直接这样用着再说?于是,就把原来的硬盘设置为启动盘,打开电源后,系统顺利启动!我原来一直担心长时间使用后,硬盘会出故障,因为硬盘高速转动且存在接触和摩擦。看来,硬盘倒是还好。至此,网站系统就都恢复了。只是这个系统是32位的(当初为了和在一台PC机上装的Linux环境兼容和互换),而且操作系统比新装的系统低了一个版本。看来,还是要找机会把系统移植到新的硬盘上,彻底进入64位的时代,以便充分发挥硬件的潜力。。。

但是,由此而产生的一个问题是,原来作为文件服务器的存储空间的一块ATA/IDE的硬盘下线了,因为新的服务器里没有ATA接口。。。由于旧服务器里不但有SATA接口,也有ATA接口数个,于是,决定继续利用一下旧服务器。为了不损害硬盘上的数据,就找出2008年退役的那台PC机,取下里面的那块ATA硬盘,作为启动盘。服务器倒是顺利启动了,并能凑合着安装一个Windows XP。而且,还从原来的旧硬盘里找到一些当时的文件。虽说没有太大的价值,总比没有好。另外,我顺便观察了一下那台旧PC机的主板,发现CPU附近的七八个电容器都坏了。这台PC机是在2004年初买的,在2008年搬家后因经常莫名其妙地掉电而退役的。我原来觉得计算机的硬盘是最容易坏的部件,因为它不停地高速转动和经常性的摩擦。不过,看来主板上的电解电容器更容易坏。。。另,这台DELL的PC机仅断断续续地使用了4年半主板上的电容就坏了,在此鄙视一下。与之相比,HP的旧服务器一直连续用了6年半才坏掉两个电容,相比而言,也算是可靠性高了(尽管我还是期望它能继续工作)。

这样,本次事件就暂时尘埃落定。之后就是把ATA硬盘上的文件拷贝出来。至于这两块ATA硬盘,可以买个硬盘接口到USB的变换连线来连到计算机上,作为外接硬盘使用。当然,为了美观的话,买个硬盘盒也是不错的选择。或者,我会把旧服务器甚至旧PC机的主板修好,让它们再次回归。

近来真是祸不单行啊。不过,事情都是有其两面性的,雨天有雨天的好处。通过这次服务器事故,获得的好处有:

  • 正式启用更强大的新服务器,而不是让它继续沉睡
  • 多了一台Windows XP虚拟机
  • 促使我尽快转为64位系统
  • 找出了一块旧硬盘,并找回一些旧文件
  • 在我有充足的时间时,旧服务器显露出问题,而不是在我忙得不可开交时出现,让我能有时间解决问题
  • 找到了旧服务器和旧PC机的问题所在,必要时有望修好
  • 提高了忧患意识,促使我尽快做出备份系统
  • 或多或少地锻炼了一下我对系统管理和诊断的能力(尽管我并不觉得很需要这种能力)
  • 促使我对负面事件的正面思考,不为世间之事迷惑

“要接受人生中的一切。人生中发生的事情都是中性的,无所谓好坏。事情的好坏取决于你如何解释它。因此,接受人生中的一切,就会获得内心安定的人生。”

“要信赖人生。只有能够信赖人生,才能获得幸福。什么是信赖人生呢?就是在心里有幸福的基准。如果认为获得他人的尊敬和有高收入是幸福,那么只要失去这些,就会立即变得不幸。信赖就是相信出现在自己的人生中的事情都是为了让自己幸福的。一般认为不幸的那些事情,到事后回想一下,那只是为了让自己更加幸福的一些契机而已。”

以上两段话出自《ユダヤ人大富豪の教え》。这两段话讲述了很深刻的道理。我学习佛法,自然对这些事情有自己的理解,但还是有点惊讶于犹太人和犹太民族文化的智慧。

根据道家的说法,“否极泰来”,当极端不幸的时候,一定要坚持住,因为这往往是黎明前的黑暗。此时,更应该高兴才是。而从佛法来说,哪有什么幸与不幸?一帆风顺的时候幸运吗?幸运怎么还会出生在这个世界?怎么会继续造下那么多的恶业?在祸不单行的时候不幸吗?不幸怎么会促进人们再次对事情深刻地思考?怎么会更加警醒,更加认识到在顺境里其实是如普贤菩萨所说“是日已过,命亦随减,如少水鱼,斯有何乐“,更加体会到”当勤精进,如救头然,但念无常,慎勿放逸“?另外,做恶梦算不幸吗?做美梦算幸运吗?说来说去都是空空如也。。。尽快从不管是美梦还是恶梦的梦境中醒来才是重要的,就像我那台新服务器一样。。。