使用Iterator简化代码2-TwoLevelIterator

May 18th, 2012 No comments

上一篇文章中,我们讲到使用Iterator可以降低Merge类的代码量,使得接口更为清晰,代码更易维护。

总的来说使用Iterator模式有下面两个好处

  • 提供简单的接口
  • 统一的访问方式

下面我们再来看一个例子,我们为一个书店写程序,书店里有许多书Book,每个书架(BookShelf)上有多本书。

类结构如下所示

class Book {
private:
 string book_name_;
};
 
 
class Shelf {
 private:
  vector<Book> books_;
};

如何遍历书架上所有的书呢?

一种实现方法是:

vector<Book>& GetBooks() const {
  return books_;
}
然后调用者会遍历所有的书

这样的实现暴漏了内部太多的细节,调用者根本就不需要知道Shelf存储Book的方式,仅仅需要遍历所有的数据即可。

而且这样当我们换用另外一种数据结构存储Book时,客户端的代码就需要进行修改。

如果使用Iterator模式则没有这个问题

// iterator.h
class Iterator {
public:
	virtual bool Valid() const = 0;
	virtual void SeekToFirst() const = 0;
	virtual void Seek(const Book& book) = 0;
	virtual void Next() = 0;
	virtual const Book& GetValue() = 0;
};
 
// shelf.h
class Shelf {
 public:
  Iterator* NewIterator() const;
 private:
  vector<Book> books_;
};
 
// shelf.cc
BookIterator : public Iterator {
 
};
 
Iterator* Shelf::NewIterator() const {
  return new BookIterator(this);
}

这种实现方式更加简单也更加的灵活.

再来看一下我们的书店(BookStore)类,一个书店中有许多个书架,我们仍然需要遍历书店中所有的书,现在应该如何实现呢?

class BookStore {
 private:
 string name_;
 vector<Shelf> shelfs_;
};

我们必须记录一些中间状态,比如遍历到了哪个书架,遍历到了书架的那本书,如果该书架的书遍历完了,要自动的移动到下一个书架上.

// 书店类
class BookStore {
 Iterator* NewIterator() const;
 private:
  vector<Shelf> shelf_;
};

一种实现方式是,由BookStore负责保存各个中间状态,包括当前遍历到了哪个书架,遍历到了书架上的那本书。

这种实现方法对外还是干净的,但是对于BookStore的维护者来说却是不友好的,Iterator的中间状态不是BookStore的成员,逻辑上不应该由BookStore维护。

更好的一种实现方式是,把遍历Iterator相关的代码封装成一个类,有两个层级Shelf 和 Book,这个类的名字我们叫做TwoLevelIteator.

TwoLevelIterator的实现(示意代码,只说明原理)
TwoLevelIterator将上面我们所说的中间状态封装成一个类,而不是散落在BookStore中作为BookStore的成员变量。

typedef Iterator* (*BookFunction)(Iterator*);
class TwoLevelIterator : public Iterator {
 public:
  TwoLevelIterator(
    Iterator* shelf_iter,
	BookFunction book_function);
  virtual ~TwoLevelIterator();
  virtual void SeekToFirst();
  virtual void Next();
  virtual void Valid();
  virtual const Book& Value() {
    return book_iter_->Value();
  }
  private:
    BookFunction book_function_;
	Iterator* shelf_iter_;
	Iterator* book_iter_;
};
 
TwoLevelIterator::TwoLevelIterator(
    Iterator* shelf_iter,
    BookFunction book_function)
	: shelf_iter_(shelf_iter),
	  book_iter_(NULL)
{
}
 
void TwoLevelIterator::SeekToFirst() {
  shelf_iter->SeekToFirst();
}
 
void TwoLevelIterator::Next() {
 book_iter->Next();
 while (!book_iter->Valid()) {
    if (!shelf_iter_->Valid()) {
	  return;
	}
   shelf_iter_->Next();
   book_iter = (*book_function)(shelf_iter);
 }
}

Leveldb中的index_block data_block,index_block是data_block的索引,这里面就用到了TwoLevelIerator.[0]

附件是一个和c++ container兼容的two_level_iterator实现(已经测试过)

使用Iterator模式来简化代码

March 24th, 2012 1 comment

我们有一个表存储了一些Record,我们使用MemTable表示在内存中的表,DiskTable表示在硬盘上的表。

需求

  1. 两个MemTable进行Merge
  2. 两个DiskTable进行Merge
  3. 一个MemTable和一个DiskTable进行Merge

一种实现方式

struct MemTable {
char* buf;
size_t len;
};
 
struct DiskTable {
char* filename;
};

DataReader是一个中间层,可以顺序读取内存表的Record和磁盘表的Record。

class DataReader {
public:
    int Init(const MemTable& memtable);
    int Init(const char* filename);
    int GetOneRecord(Record* record);
};

DataMerge做实际的Merge工作,有三个Init函数。

class DataMerger {
public:
    void Init(MemTable& m1, MemTable& m2);
    void Init(DiskTable& f1, DiskTable& f2);
    void Init(MemTable& m, MemTable& f);
    int DoMerge();
private:
    enum MergeType{MEM_TO_MEM, DISK_TO_DISK, FILE_TO_FILE};
    int MergeMemoryMemory();
    int MergeDiskDisk();
    int MergeMemoryDisk();
};

分析

上面的设计是一个非常糟糕的设计

  1. MemTable DiskTable
    1. 这两个类没有实际的数据处理函数
    2. 所有的数据操作由DataReader完成
  2. DataReader知道了太多的细节
  3. DataMerger
    1. 函数的接口复杂:有三个Init函数
    2. 扩展性差:如果新增一种Table的实现方式,涉及到的改动太多
  4. 实际做Merge工作的函数有大量重复代码,但是其实只有读取数据的方式是不一样的

Iterator实现

一种更好的设计模式是:使用Iterator设计模式可以让代码变得更加容易维护

Iteartor模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

对于上面提到的三点我们分别进行处理

  1. 由MemTable DiskTable负责数据的读取工作,不要把这个内部操作暴露给其他的类
  2. 去掉DataReader,它的工作分别由MemTable和DiskTable完成
  3. DataMerger只针对接口编程(Iterator接口)
class Iterator {
public:
	virtual bool Valid() const = 0;
	virtual void SeekToFirst() const = 0;
	virtual void Next() = 0;
	virtual const Record& GetRecord() = 0;
};
 
class MemTable {
public:
        // 由MemTable生成Iterator,来做具体的数据读取工作
        // 去掉DataReader 类
	Iterator* NewIterator() const;
	// 其他的数据操作
	// Append Delete Find
private:
	// 这时我们已经不关注MemTable的具体实现方式了
	char* buf;
	size_t len;
};
 
class DiskTable {
public:
        // 由DiskTable管理具体的数据读取工作,去掉DataReader类
	Iterator* NewIterator() const;
	int Init(char* filename);
private:
	char* filename;
};
 
// 这里我们去掉了DataMerger类,数据的Merge操作全部完全不依赖于Table具体的实现方式
// 更加容易扩展,增加任意多的Table类型对Merge算法都没有影响
Iterator* NewMergingIterator(Iterator** list, int n);

总结

  • Iterator设计模式大大简化了代码的数量(Merger类)
  • Iterator模式以一种统计的方式来访问对象,屏蔽了数据细节
Categories: c+, leveldb, 设计模式, 默认 Tags:

数据库分区技术

March 16th, 2012 3 comments

当数据量、访问量都很小的时候单机的mysql就可以承受,随着网站规模的扩大,单机就扛不住了,需要多台机器共同解决。通常可以使用分区的方法来解决这个问题。这里我们从系统的可扩展性、负载均衡两个方面考虑SQL数据库分区技术的优劣。

分区从数学上就是id到所在分区的函数
id --> sharding
单机时就是: 即所有的用户都落在一台机器上
shardingfunction(id) = 0
多机的情况就是:
shardingfunction(id) = ?
常见的sharding方法有取余、范围、映射表的方法。

假设我们的论坛有一张用户表和帖子表

CREATE TABLE USER
(
user_id BIGINT UNSIGNED NOT NULL,
name CHAR(256),
email CHAR(256),
age SMALLINT,
register_time DATETIME,
PRIMARY KEY(user_id)
)
 
CREATE TABLE post
(
post_id BIGINT UNSIGNED NOT NULL,
title TEXT,
user_id BIGINT UNSIGNED,
body TEXT,
creat_time DATETIME,
PRIMARY KEY(post_id)
)

取余分区

sharding_number = user_id % 64

Pros.

这种方法非常简单,而且各个分区上的用户数也大概均衡

Cons.

  • 负载不均衡
    • 有些用户的数据量大,有些用户的数据量小(某个分区上有水王,特别能发帖)
  • 可扩展性差
    • 增加机器时必须重新分配数据

范围分区

按照user_id将0-999为第一个分区,1000-1999为第二2分区

Pros.

  • 可扩展性好

Cons.

  • 负载不均衡
    • 老用户所在的分区压力大,新用户所在的分区压力小

映射关系分区

将user_id所在的分区存储在数据库,放访问user_id为198用户的数据时,首先找到user_id所在的分区,然后再去访问

Pros.

  • 可扩展性好
  • 负载均衡
    • 添加一个新的用户时,可以选出负载最小的分片

Cons.

  • 比取余复杂

映射关系分区的实现方式

数据库有4个sharding,一个Metaserver记录了数据库的信息包括静态信息:主要是数据库的分区情况;动态信息:主要是数据库的负载情况。

2个同构的Server作为数据的前端。


启动时,Server访问4个sharding获得userid和sharding的映射关系(user_id --> sharding)

注意到只有当新增用户时映射关系表才会发生变化,映射表相当于数据库分区信息的Cache。
Server1和Server2的user_id->sharding映射表的短暂不一致,并不会影响数据操作。

增加(Insert)

  • 新加一个user,首先会根据4个sharding的负载情况选择合适的一组
  • 将新增的user信息写入数据库
  • 写入本地映射表

查找(Select)

  • 查找映射表获得user_id对应的sharding
  • 如果在映射表中没有user_id的信息,查找数据库将user_id的分区信息插入到映射表中
  • 查找数据库获得user信息

删除(Delete)

  • 和查找类似

改动(Upate)

  • 和查找类似

对于两个Server之间的映射表如何保持一致,上面采取了一种最为简单的方法。
如果为了减少数据库的访问还可以这样做

  • 主动分享
    • Server1上增加了一个用户,那么server1会向其他的server广播新增的用户id
  • 积极询问
    • Server2发现一个user_id不再自己的映射关系表中,那么就向其他的server广播询问

映射关系表数据结构

如何选择一种有效的数据结构来存储这种映射关系呢?

用户数	500W	1000W	2000W
数组	42M	85M	172M
Hash	84M	172M	343M

这个数量级的内存对于我们来说是可以接受的

是否有更为高效的数据结构来存储sharding>的映射关系呢?

Categories: 数据库, 默认 Tags:

结构体签名与内存空洞

March 3rd, 2012 No comments

使用C/C++时总会遇到各种很奇怪的现象,这不我我就遇到了一个。

同一个结构体,在不同时刻计算签名就会返回不同值的奇怪现象。

使用如下函数计算MemoryHole结构体的签名,结果同一个结构体MemoryHole在不同的时候调用CreateSign函数,就返回不同的结果。CreateSign函数是可重入的,因此肯定不是多线程的问题。

struct MemoryHole {
    uint32_t a;
    uint64_t b;
};
 
int64_t CreateSign(const MemoryHole& hole);

使用下面的测试程序:

void PrintArray(unsigned char* a, size_t length) {
    for (size_t i = 0; i < length; i++) {
        cout << (int)a[i] << " ";
    }
    cout << endl;
}
int main() {
    cout << sizeof(Hole) << endl;
    unsigned char byte[] = {1, 0, 0, 0, 3, 2, 1, 0,
                            13,0, 0, 0, 0, 0, 0, 0};
    Hole* hole = (Hole*)byte;
    cout << "hole " << hole->a << ", " << hole->b << endl;
    PrintArray((unsigned char*)hole, sizeof(Hole));
 
    Hole ahole = *hole;
    cout << "ahole" << ahole.a << ", " << ahole.b << endl;
    PrintArray((unsigned char*)&ahole, sizeof(Hole));
 
    Hole bhole;
    memset(&bhole, 0, sizeof(bhole));
    bhole.a = hole->a;
    bhole.b = hole->b;
    cout << "bhole " << bhole.a << ", " << bhole.b << endl;
    PrintArray((unsigned char*)&bhole, sizeof(Hole));
    return 0;
 
}

结果:我们发现地4-7字节并不会影响MemoryHole中a b的取值。

16
hole 1, 13
1 0 0 0 3 2 1 0 13 0 0 0 0 0 0 0
ahole1, 13
1 0 0 0 3 2 1 0 13 0 0 0 0 0 0 0
bhole 1, 13
1 0 0 0 0 0 0 0 13 0 0 0 0 0 0 0

MemoryHole是的内存排列是:第4-7字节是不确定的,如果直接在MemoryHole是算签名就会出错。

0-7	 |   a 	 | 不确定 |
8-15     | b_low | b_high |

那么我们应该如何正确得到MemoryHole结构体的签名呢?

方法1:使用pragma宏,这样可以按照字节对齐,从而避免了内存空洞

#pragma pack(push, 1)
struct MemoryHole {
    uint32_t a;
    uint64_t b;
};
#pragma pack(pop)

方法2:在使用前memset一下。

void SetMemoryHole(const MemoryHole& a, MemoryHole* b) {
	memeset(b, 0, sizeof(MemoryHole));
	b->a = a.a;
	b->b = a.b;
}

foreach的c++实现

January 13th, 2012 No comments

(灌水)
C#和java中都有foreach,foreach可以减少代码的长度和复杂性让程序更为简单。

int main() {
    int a[] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    vector<int> v(a, a + sizeof(a) / sizeof(a[0]));
    foreach (int x, v) {
        cout << x << endl;
    }
    return 0;
}

c++的实现方法为(copy QT代码)

template <typename T>
class ForeachContainer {
public:
    inline ForeachContainer(const T& t) : brk(0), i(t.begin()), e(t.end()) { }
    int brk;
    typename T::const_iterator i, e;
};
 
#define FOREACH(variable, container)                                  \
for (ForeachContainer<__typeof__(container)> _container_(container);  \
     !_container_.brk && _container_.i != _container_.e;              \
     __extension__  ({ ++_container_.brk; ++_container_.i; }))        \
    for (variable = *_container_.i;; __extension__ ({--_container_.brk; break;}))
 
#define foreach FOREACH
Categories: c+, 默认 Tags:

由Chrome的日历插件想到的

December 21st, 2011 1 comment

Chrome的插件万年历很方便,安装这个插件后,查农历就不用在不用在浏览器中搜索万年历,然后再点击一个页面才能查看。直接点一下就可以看日历了。这个插件也可以记事,美中不足的是不能够同步。


谷歌日历是一款比较好的日历工具,它最大的特点就在于可以同步。可以在网页上编辑好自己的日程,然后在Android/iphone/win7手机上同步一下就可以看到自己的日程信息了。但是感觉谷歌日历没有农历,不太符合中国人的使用习惯,

我希望有这么一款日历产品。

  • 支持同步功能。这个就不用解释了,在云计算的时代,不能同步的产品不能称之为好产品。
  • 和task list结合在一起。这样会更加方便。
  • 支持中国农历。这个谷歌日历网页版有农历,但是android客户端是没有农历,需要自己订阅一个农历信息,这样就加大了用户的负担,让你的用户选择别的产品。
  • 农历提醒。像我是过农历生日的。在谷歌日历上就不能设置农历生日提醒,除了QQ有这个功能外,大多数的im软件都没有这个功能。
  • 节假日提醒。现在快元旦了,想知道元旦放几天假,在baidu上搜索元旦假期就能出来下面的日历,可以直接看到放假信息,既有公历又有农历,非常方便。但是和Chrome的插件相比,它还是多了一个操作,你要在浏览器里搜索,而日历插件只要点一下就行了。


谷歌日历通过订阅某个日历得到,但是谷歌官方也没有这样的共享日历可以订阅,真是遗憾。

  • 大公司,提供一整套日历产品工具。一整套的产品让人们用着方面,比如谷歌日历、Todo list,有这么一套的行程管理工具。而且希望是大公司提供技术支持,这让用户会更加放心把日历同步到你的服务器上,不用过分担心数据被盗,小公司垮掉的问题。

像365日历,就做了农历+同步(包括同步谷歌日历)的功能,还有许多用户再用,我下载用了一小会,最终还是删掉了。主要是觉得它是个小公司,感觉不太靠谱。而且这款产品在其他日程管理方面,并没有比谷歌日历,有明显优势。

今年Baidu出了易手机,做了许多本地方的修改,更加贴合中国人的使用习惯,不知道日历功能怎么样。

Categories: 默认 Tags:

2011年书目整理

December 16th, 2011 1 comment

武侠小说中每个梦想达到武林至高点的英雄好汉,莫不在寻找一本武林秘籍,为了得到这部武林秘籍,不惜付出自己的性命。《九阴真经》引得东邪、西毒、南帝北丐等人大大出手,而《葵花宝典》更是让许多掌门人做了一个痛苦的决定。

在网络高度发达的今天我们可以很方便的阅读各种书籍不用付出什么惨重代价,工作快一年了,为了追求心中的“武林至尊”,看了一些秘籍,与大家分享一下。

unix环境高级编程'[0]其实这本书更像是一本工具书,遇到弄不清楚的地方就快速的查一下,但是用这本书学习unix编程,效果可能不是很好。

推荐一本好书《深入理解计算机系统[1],(其实是上学的时候看的)就是一本比较好的书了,美国多所著名高校都用它来作为计算机编程的教材。哈佛大学的CS61课程的教材,PPT中把整个计算机课程分为2类:CS61和其他的课程,可见这门课的重要性。

可以配合Havard的在线课程[2]。书中有8个实验,都很有趣。比如二进制炸弹,让你过一把黑客瘾,总共有6关,运行时需要输入6个不同的字符串,如果有一个输入错误,炸弹就会爆炸。需要对程序进行反汇编和逆向工程来测定是哪一个字符串。缓冲区溢出实验,可以通过研究缓冲区溢出的错误,来修改二进制可执行文件的运行行为。Shell实验要求自己动手写一个shell,代理实验是自己写一个并行的web代理。

unix编程艺术[3],这本书没有讲具体的编程技巧,而是讲述了Unix的发展、Unix的编程文化。其中第一张总结的16个Unix哲学更是全书的精华所在。这里我忍不住要在这里在重复一下:

  1. 模块原则:使用简洁的借口拼合简单的部件。
  2. 清晰原则:清晰胜于机巧
  3. 组合原则:设计时请考虑拼接组合
  4. 分离原则:策略同机制分离,接口同实现分离
  5. 简洁原则:设计要简洁,复杂度能低则低
  6. 吝啬原则:除非确无它法,不要编写庞大的程序
  7. 透明性原则:设计要可见,以便审查和调试
  8. 健壮原则:健壮源于透明和简洁
  9. 表示原则:把知识叠入数据以求逻辑质朴和健壮
  10. 通俗原则:接口设计避免标新立异
  11. 缄默原则:如果程序没什么好说的,就沉默
  12. 补救原则:程序出现异常时,马上退出并给出足够错误信息
  13. 经济原则:宁花机器一分,不花程序员一秒
  14. 生成原则:避免手工hack,尽量编写程序去生成程序
  15. 优化原则:雕琢前先要有原型,跑之前先要会走
  16. 多样性原则:绝不相信不二法门的断言
  17. 扩展原则:设计着眼未来,未来总比预想来的快

一言蔽之就是KISS原则:Keep It Simple, Stupid!

重构:改善既有代码的设计》,这是一本非常实用的好书。现在作为一个程序员,更多的会是维护公司的老代码,在老代码的基础上进行BUG修复、功能升级,完全从头开始做一个新项目的机会不太多。如果你现在维护的代码面临着:大量重复代码、函数很长、参数过多、许多的switch、功能相似的类等问题时,那么看看这本书,绝对可以帮助你。

Guide:Writing Testable Code[4]Misko Hevery是Google的测试工程师,这份文档讲述了Google的怎样做单测,如何进行设计让一个类更易于单测。一个不容易单测的设计肯定不好的设计,文档给出了许多不易单测的例子,通过重构让这些例子更加容易单测,这份文档和《重构:改善既有代码的设计》相互补充,从单测的角度告诉你应该如何重构你的代码。

leveldb源代码[5]leveldb是Google实现的一个高性能的KV数据库,相当于Bigtable中的一个tablet。阅读源码让我们既可以学习如何设计一个高效的KV数据库,又可以学习Google的编码风格。结合Google C++ Code Style来阅读源代码会对Google的C++编程规范印象更深,也更容易学习。leveldb在实现时有许多Trick的地方比如为了减少编译依赖许多类的内部都有struct Rep结构,结合这个再来阅读《Effective C++》会让你对Effetive c++中所列出的条款有更深的理解。

怪诞行为学:可预测的非理性》,老罗在海淀剧院演讲时推荐的书,书中通过一个个实例介绍了奸商如何做营销,看了这本书如果你是商人那么会让你如虎添翼,如果是普通消费者也可以避免奸商设下的各种圈套。

编程珠玑续[6],这本和《编程珠玑》[7]有许多重复的内容,而新加的内容感觉也没有《编程珠玑》那么让人震撼,建议就不要看了。

Categories: 好书推荐, 默认 Tags:

信息熵与热力学熵

December 2nd, 2011 No comments

化学及热力学中所指的熵,是一种测量在动力学方面不能做功的能量总数。熵亦被用于计算一个系统中的失序现象,用来衡量一个系统混乱程度的度量。

热力学熵

熵是什么呢?宏观上--体系的熵等于可逆过程吸收或耗散的热量除以它的绝对温度,也就是一种测量在动力学方面不能做功的能量总数。微观上--熵是大量微观粒子的位置和速度的分布概率的函数,是描述系统中大量微观粒子的无序性的宏观参数。

举例来讲果我们能看到橡皮筋的分子结构,我们会发现它的结构在拉紧和放松的状态时是不一样的。放松的时候它的分子结构像一团乱麻交织在一起。而在把橡皮筋拉长的时候,那些如同链状的分子就会沿着拉伸的方向比较整齐地排列起来。于是我们可以看到两种状态:一种是自然,或者自发的状态。在这种状态下结构呈混乱或无序状。而另一种是在外界的拉力下规则地排列起来的状态。这种无序的状态还可以从分子的扩散中观察到。用一个密封的箱子,中间放一个隔板。在隔板的左边空间注入烟。我们把隔板去掉,左边的烟就会自然(自发)地向右边扩散,最后均匀地占满整个箱体。这种状态称为无序。

在物理学里我们可以用熵的概念来描述某一种状态自发变化的方向。比如把有规则排列的状态称为低熵而混乱的状态对应于高熵而熵则是无序性的定量量度。热力学第二定律的结论是:一个孤立系统的熵永不减少。换句话说,物质世界的状态总是自发地转变成无序;从低熵变到高熵。比如,当外力去除之后,整齐排列的分子就会自然地向紊乱的状态转变;而箱子左边的烟一定会自发地向右边扩散。这就是著名的熵增定律,熵增原理表示自然界会越来越无序。

信息熵

那么信息熵是什么呢?

一个 X 值域为{x_1, ..., x_n}的随机变量的熵值 H 定义为:

H(X) = E(I(X)),

其中,E 代表了期望函数,而 I(X) 是 X 的信息量(又称为信息本体)。I(X) 本身是个随机变量。如果 p 代表了 X 的机率质量函数(probability mass function),则熵的公式可以表示为:

 H(X)={\sum_{i=1}^n p(x_i)I(x_i)}= - {\sum_{i=1}^n p(x_i)log_bp(x_i)}

信息熵可以认为是系统中所含有的平均信息量大小,也可以认为是描述一个系统需要的最小存储空间长度,即最少用多少个存储空间就可以描述这个系统。

信息熵与热力学中的熵有什么关系呢?

举一个高中课本上的例子,我们存放在抽屉中的火柴,火柴都是整齐排列的,这时熵比较小;散落在地上的火柴是混乱,熵 比较大。

同样,放在抽屉中的火柴我们用来描述它的所需要的存储单元就少,我们可以用一句话就可以描述;50根火柴朝右。但是散落在地上的火柴,却需要这样描述,有50根火柴,其中10根朝向左,10根朝向右,10根朝上,20根朝下……。

可见:信息熵和热力学熵是正相关的,热力学熵越大,系统越混乱,需要用越多的存储单元来描述,信息熵也就越大;热力学熵越小,系统越有序,需要越小的存储单元来描述,信息熵也就越小。

最小编码长度

学数据结构时我们都学过huffman编码,比如有
P(X=A) = 1/2 P(X=B) = 1/4 P(X=C) = 1/8 P(X=D) = 1/8
信息熵为:-1/2log1/2-1/4log1/4-1/8log1/8-1/8log1/8=1.75
huffman编码所要解决的问题是如何编码获得最小的编码长度,可以证明huffman编码就是满足最小信息熵的编码。

最大熵原理

在机器学习中经常用到最大熵原理:我们有以下限制
max S = -p_1 logp_1 - p_2 logp_2
p_1 + p_2 = 1
求什么情况下信息熵会最大

任何物质系统除了都受到或多或少的外部约束外,其内部总是具有一定的自由度,这种自由度导致系统内的各元素处于不同的状态。而状态的多样性,状态的丰富程度(混乱程度、复杂程度)的定量计量标尺就是熵,熵最大就是事物状态的丰富程度自动达到最大值。换句话说,事物总是在约束下争取(或呈现)最大的自由权,也就是保留全部的不确定性把风险降到最低,不要把鸡蛋放在同一个篮子里说的也就是这个道理。

最大熵原理:也就是承认事物已知的约束条件,对事物未知的约束条件不带有任何假设和偏见。这样子概率分布最均匀,整个系统能够产生的状态也就越多,整个系统越混乱,描述系统也需要的存储空间越大,熵越大,信息熵也就越大。

在决策树算法中用到了最大熵的原理:决策树是为了解决分类问题,分类的过程其实是熵减少的过程,让原先混杂在一起的类找到相应的类别,因此每次我们应该选择具有最小上的分类面。比如: 如果选择 B 作为分类面,那么左右两边0 1 的个数相同这时候熵最大,而选择A或C做分类面则熵比较小。
(在数据挖掘或者机器学习的书中会选择具有最大信息增益的分界面,其实是一个道理)

	            A      B     C
		0 0 | 1 1  | 0 0 | 1 1
		0 0 | 1 1  | 0 0 | 1 1

最终的决策树可以是这个样子:

				 x <= A
				  / \
				0   x <= B
				    /  \
				   1   x <= C
					/\
			               0   1

虽然按照最大信息熵选出的分类面最容易把事物分开,但是决策树的高度会很大,因此在预测阶段我们可以调整树的结构从而达到较快的预测速度。

参考文献

http://www.autonlab.org/tutorials/infogain.html
http://zh.wikipedia.org/wiki/%E7%86%B5
http://zh.wikipedia.org/wiki/%E7%86%B5_(%E4%BF%A1%E6%81%AF%E8%AE%BA)

附:

<信息熵与统计力学中熵关系的简单证明>
在统计力学中,玻尔兹曼发现单一系统中的熵跟构成热力学性质的微观状态数量相关。
S = k(ln \Omega)
Ω则为该宏观状态中所包含之微观状态数量。

统计力学熵:
有A B两个粒子它们总共有6种状态,
max S = k(ln\Omega)
  x + y = 6
  \Omega = x * y 即:S = k ln (x * y), 由于我们之关系信息熵与热力学熵的关系,
为了和信息熵比较我们令p_1 = x/6, p_2 = y/6
热力学熵 S = k ln(x * y) = k ln x/6 * y/6 * 6^2 = k * (ln p_1 + ln p_2 + 2 ln 6)

信息熵:
max S = -p_1 log p_1 - p_2 log p_2
p_1 + p_2 = 1
函数图象为:

可见,热力学熵与信息熵正相关,且在p_1 = p_2 = 1/2时取到最大值,此时系统的整体状态最多,描述这个系统需要的存储单元也最长。

Categories: 科普, 默认 Tags:

LevelDB源码分析之 Version VersionEdit VersionSet

November 17th, 2011 No comments

Version 保存了当前磁盘以及内存中所有的文件信息,一般只有一个Version叫做"current" version(当前版本)。

Leveldb还保存了一系列的历史版本,这些历史版本有什么作用呢?

当一个Iterator创建后,Iterator就引用到了current version(当前版本),只要这个Iterator不被delete那么被Iterator引用的版本就会一直存活。这就意味着当你用完一个Iterator后,需要及时删除它。

当一次Compaction结束后(会生成新的文件,合并前的文件需要删除),Leveldb会创建一个新的版本作为当前版本,原先的当前版本就会变为历史版本。

VersionSet 是所有Version的集合,管理着所有存活的Version。

VersionEdit 表示Version之间的变化,相当于 delta 增量,表示有增加了多少文件,删除了文件。下图表示他们之间的关系。

Version0 + VersionEdit --> Version1 
 
Version0->Version1->Version2->Version3

VersionEdit会保存到MANIFEST文件中,当做数据恢复时就会从MANIFEST文件中读出来重建数据。

leveldb的这种版本的控制,让我想到了双buffer切换,双buffer切换来自于图形学中,用于解决屏幕绘制时的闪屏问题,在服务器编程中也有用处。

比如我们的服务器上有一个字典库,每天我们需要更新这个字典库,我们可以新开一个buffer,将新的字典库加载到这个新buffer中,等到加载完毕,将字典的指针指向新的字典库。

leveldb的version管理和双buffer切换类似,但是如果原version被某个iterator引用,那么这个version会一直保持,直到没有被任何一个iterator引用,此时就可以删除这个version了。

Categories: leveldb, 默认 Tags:

find和rm引发的血案

November 17th, 2011 No comments

今天在windows中清理磁盘,删除所有*.obj文件,obj文件在不同的文件夹中。
在cygwin中输入了如下命令:

find . name "*.obj" | xargs rm

执行完之后发现文件加下所有的文件都被删掉,只剩下目录了。
原来命令输入错了,应该是

find . -name "*.obj" | xargs rm

find . -name 这条命令会递归打印所有的文件、name文件、"*.obj"文件(name和"*.obj"不存在,会提示找不到)。配合rm将所有的文件删了个干净。

这真是一条杀伤力巨大的命令,把我目录中所有的文件都给删没了。

rm是一条及其危险的命令,如果提供类似于windows的回收站机制会更方面人们的使用。

Categories: Linux Tags: