2015年2月26日
#
//targetver.h
#pragma once
// 包括 SDKDDKVer.h 將定義可用的最高版本的 Windows 平臺。
// 如果要為以前的 Windows 平臺生成應用程序,請包括 WinSDKVer.h,并將
// WIN32_WINNT 宏設置為要支持的平臺,然后再包括 SDKDDKVer.h。
#include <WinSDKVer.h>
#define _WIN32_WINNT _WIN32_WINNT_WINXP
#include <SDKDDKVer.h>
1、如上:在targetver.h中添加代碼
2、如下:修改運行庫
2014年12月18日
#
由于項目原因,開始學習C++,剛接觸半個多月,今天參考網上例子,寫了個簡單的C++連接ORACLE的DEMO,可是使用g++編譯時不順利,不是報這個錯就是那個,最后參考網上的解決方式和個人理解,終于調試好了,現把編譯中出現的問題和解決方法總結出來。 源代碼 - #include <iostream>
- #include <string>
- #include "occi.h"
- using namespace oracle::occi;
- using namespace std;
-
- int main()
- {
- string usr="sys";
- string pwd="orcl";
- string SID="ORCL";
- string date;
-
- Environment *env=Environment::createEnvironment(Environment::OBJECT);
- Connection *conn= env->createConnection(usr,pwd,SID);//all strings
- if(conn)
- cout<<"success createConnection!"<<endl;
- else
- cout<<"failure createConnection!"<<endl;
-
- Statement *stmt = conn->createStatement();
- string sSQL = "select to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') from dual";
- stmt->setSQL(sSQL);
-
-
- ResultSet *rs = stmt->executeQuery();
- if(rs->next())
- {
- date = rs->getString(1);
- }
-
- cout<<"now time :"<<date<<endl;
-
- env->terminateConnection(conn);
- Environment::terminateEnvironment(env);
-
- return 0;
- }
-
- #include <iostream>
- #include <string>
- #include "occi.h"
- using namespace oracle::occi;
- using namespace std;
-
- int main()
- {
- string usr="sys";
- string pwd="orcl";
- string SID="ORCL";
- string date;
-
- Environment *env=Environment::createEnvironment(Environment::OBJECT);
- Connection *conn= env->createConnection(usr,pwd,SID);//all strings
- if(conn)
- cout<<"success createConnection!"<<endl;
- else
- cout<<"failure createConnection!"<<endl;
-
- Statement *stmt = conn->createStatement();
- string sSQL = "select to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') from dual";
- stmt->setSQL(sSQL);
-
-
- ResultSet *rs = stmt->executeQuery();
- if(rs->next())
- {
- date = rs->getString(1);
- }
-
- cout<<"now time :"<<date<<endl;
-
- env->terminateConnection(conn);
- Environment::terminateEnvironment(env);
-
- return 0;
- }
-
本人linux上安裝oracle路徑:/opt/app/oracle/product/10.2.0/db_1 編譯命令:g++ -o conn -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -g 問題一:編譯時報如下錯誤: - [oracle@localhost demo]$ g++ g++ -o conn -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib -lclntsh -locci /usr/lib/libstdc++.so.5 conn_db.cpp -g
- g++: g++: No such file or directory
- conn_db.cpp:3:18: error: occi.h: No such file or directory
- conn_db.cpp:4: error: 'oracle' has not been declared
- conn_db.cpp:4: error: 'occi' is not a namespace-name
- conn_db.cpp:4: error: expected namespace-name before ';' token
- conn_db.cpp: In function 'int main()':
- conn_db.cpp:14: error: 'Environment' was not declared in this scope
- conn_db.cpp:14: error: 'env' was not declared in this scope
- conn_db.cpp:14: error: 'Environment' is not a class or namespace
- conn_db.cpp:14: error: 'Environment' is not a class or namespace
- conn_db.cpp:15: error: 'Connection' was not declared in this scope
- conn_db.cpp:15: error: 'conn' was not declared in this scope
- conn_db.cpp:21: error: 'Statement' was not declared in this scope
- conn_db.cpp:21: error: 'stmt' was not declared in this scope
- conn_db.cpp:26: error: 'ResultSet' was not declared in this scope
- conn_db.cpp:26: error: 'rs' was not declared in this scope
- conn_db.cpp:35: error: 'Environment' is not a class or namespace
-
解決:編譯時沒有引入OCCI頭文件,如果沒有,先下載對應的 ORACLE client安裝,比如我的是oracle10g,下載了oracle-instantclient-basic- 10.2.0.4-1.i386.zip,解壓到一個目錄下(/home/oracle/oracle/include),然后在編譯文件的時候引進這個解壓目錄 編譯命令增加OCCI目錄:g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -g 問題2:找不到對應函數 - [oracle@localhost demo]$ g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -Wall -O -g
- /tmp/cclFs9xq.o: In function `main':
- /home/oracle/oracle/demo/conn_db.cpp:14: undefined reference to `oracle::occi::Environment::createEnvironment(oracle::occi::Environment::Mode, void*, void* (*)(void*, unsigned int), void* (*)(void*, void*, unsigned int), void (*)(void*, void*))'
- /home/oracle/oracle/demo/conn_db.cpp:35: undefined reference to `oracle::occi::Environment::terminateEnvironment(oracle::occi::Environment*)'
- collect2: ld returned 1 exit status
-
解決:增加libocci.so和libclntsh.so指定編譯 修改后的編譯命令: g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -lclntsh -locci -Wall -O -g 另外可能在引入-lclntsh -locci編譯時可能會報找不到以下錯誤: - [oracle@localhost demo]$ g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -lclntsh -locci /usr/lib/libstdc++.so.5 -Wall -O -g
- /usr/bin/ld: cannot find -lclntsh
- collect2: ld returned 1 exit status
- [oracle@localhost demo]$
-
解決:這是因為沒有找到libclntsh.so和libocci.so鏈接庫,你在可以把oracle client安裝目錄下把這兩個文件拷貝到$ORACLE_HOME/lib目錄下,或加到/usr/lib目錄下就可以了 問題三:occi在linux編譯運行時報libstdc++.so.6沖突的問題 - [oracle@localhost demo]$ g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -lclntsh -locci -Wall -O -g
- /usr/bin/ld: warning: libstdc++.so.5, needed by /opt/app/oracle/product/10.2.0/db_1/lib/libocci.so, may conflict with libstdc++.so.6
- [oracle@localhost demo]$ g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -lclntsh -locci -Wall -O -g
- /usr/bin/ld: warning: libstdc++.so.5, needed by /opt/app/oracle/product/10.2.0/db_1/lib/libocci.so, may conflict with libstdc++.so.6
解決:OCCI庫在linux編譯的時候,由于linux版本太高,會提示以上情況,實際上,在大多數linux系統上,還保留有libstdc++5的庫,自己手工在編譯的時候加上去就好了 修改后的編譯命令:g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib -lclntsh -locci /usr/lib/libstdc++.so.5 conn_db.cpp -g 編譯通過后執行結果輸出: - [oracle@localhost demo]$ g++ -o conn -I/home/oracle/oracle/include -L/opt/app/oracle/product/10.2.0/db_1/lib -L/opt/oracle/product/10.2.0/db_1/rdbms/lib conn_db.cpp -lclntsh -locci /usr/lib/libstdc++.so.5 -Wall -O -g
- [oracle@localhost demo]$ ./conn
- success createConnection!
- now time :2010-11-14 22:49:24
- [oracle@localhost demo]$
2014年11月10日
#
常用的字符串Hash函數還有ELFHash,APHash等等,都是十分簡單有效的方法。這些函數使用位運算使得每一個字符都對最后的函數值產生影響。另外還有以MD5和SHA1為代表的雜湊函數,這些函數幾乎不可能找到碰撞。
常用字符串哈希函數有BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,PJWHash,ELFHash等等。對于以上幾種哈希函數,我對其進行了一個小小的評測。
Hash函數 | 數據1 | 數據2 | 數據3 | 數據4 | 數據1得分 | 數據2得分 | 數據3得分 | 數據4得分 | 平均分 |
BKDRHash | 2 | 0 | 4774 | 481 | 96.55 | 100 | 90.95 | 82.05 | 92.64 |
APHash | 2 | 3 | 4754 | 493 | 96.55 | 88.46 | 100 | 51.28 | 86.28 |
DJBHash | 2 | 2 | 4975 | 474 | 96.55 | 92.31 | 0 | 100 | 83.43 |
JSHash | 1 | 4 | 4761 | 506 | 100 | 84.62 | 96.83 | 17.95 | 81.94 |
RSHash | 1 | 0 | 4861 | 505 | 100 | 100 | 51.58 | 20.51 | 75.96 |
SDBMHash | 3 | 2 | 4849 | 504 | 93.1 | 92.31 | 57.01 | 23.08 | 72.41 |
PJWHash | 30 | 26 | 4878 | 513 | 0 | 0 | 43.89 | 0 | 21.95 |
ELFHash | 30 | 26 | 4878 | 513 | 0 | 0 | 43.89 | 0 | 21.95 |
其中數據1為100000個字母和數字組成的隨機串哈希沖突個數。數據2為100000個有意義的英文句子哈希沖突個數。數據3為數據1的哈希值與1000003(大素數)求模后存儲到線性表中沖突的個數。數據4為數據1的哈希值與10000019(更大素數)求模后存儲到線性表中沖突的個數。
經過比較,得出以上平均得分。平均數為平方平均數。可以發現,BKDRHash無論是在實際效果還是編碼實現中,效果都是最突出的。APHash也是較為優秀的算法。DJBHash,JSHash,RSHash與SDBMHash各有千秋。PJWHash與ELFHash效果最差,但得分相似,其算法本質是相似的。
在信息修競賽中,要本著易于編碼調試的原則,個人認為BKDRHash是最適合記憶和使用的。
BYVoid原創,歡迎建議、交流、批評和指正。
附:各種哈希函數的C語言程序代碼unsigned int SDBMHash(char *str)
{
unsigned int hash = 0;
while (*str)
{
// equivalent to: hash = 65599*hash + (*str++);
hash = (*str++) + (hash << 6) + (hash << 16) - hash;
}
return (hash & 0x7FFFFFFF);
}
// RS Hash Function
unsigned int RSHash(char *str)
{
unsigned int b = 378551;
unsigned int a = 63689;
unsigned int hash = 0;
while (*str)
{
hash = hash * a + (*str++);
a *= b;
}
return (hash & 0x7FFFFFFF);
}
// JS Hash Function
unsigned int JSHash(char *str)
{
unsigned int hash = 1315423911;
while (*str)
{
hash ^= ((hash << 5) + (*str++) + (hash >> 2));
}
return (hash & 0x7FFFFFFF);
}
// P. J. Weinberger Hash Function
unsigned int PJWHash(char *str)
{
unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);
unsigned int ThreeQuarters = (unsigned int)((BitsInUnignedInt * 3) / 4);
unsigned int OneEighth = (unsigned int)(BitsInUnignedInt / 8);
unsigned int HighBits = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);
unsigned int hash = 0;
unsigned int test = 0;
while (*str)
{
hash = (hash << OneEighth) + (*str++);
if ((test = hash & HighBits) != 0)
{
hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));
}
}
return (hash & 0x7FFFFFFF);
}
// ELF Hash Function
unsigned int ELFHash(char *str)
{
unsigned int hash = 0;
unsigned int x = 0;
while (*str)
{
hash = (hash << 4) + (*str++);
if ((x = hash & 0xF0000000L) != 0)
{
hash ^= (x >> 24);
hash &= ~x;
}
}
return (hash & 0x7FFFFFFF);
}
// BKDR Hash Function
unsigned int BKDRHash(char *str)
{
unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
// DJB Hash Function
unsigned int DJBHash(char *str)
{
unsigned int hash = 5381;
while (*str)
{
hash += (hash << 5) + (*str++);
}
return (hash & 0x7FFFFFFF);
}
// AP Hash Function
unsigned int APHash(char *str)
{
unsigned int hash = 0;
int i;
for (i=0; *str; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
}
}
return (hash & 0x7FFFFFFF);
}
2014年5月26日
#
JSON(JavaScript Object Notation)跟xml一樣也是一種數據交換格式,了解json請參考其官網http://json.org/,本文不再對json做介紹,將重點介紹c++的json解析庫的使用方法。json官網上列出了各種語言對應的json解析庫,作者僅介紹自己使用過的兩種C++的json解析庫:jsoncpp(v0.5.0)和Boost(v1.34.0)。
一. 使用jsoncpp解析json
Jsoncpp是個跨平臺的開源庫,首先從http://jsoncpp.sourceforge.net/上下載jsoncpp庫源碼,我下載的是v0.5.0,壓縮包大約107K,解壓,在jsoncpp-src-0.5.0/makefiles/vs71目錄里找到jsoncpp.sln,用VS2003及以上版本編譯,默認生成靜態鏈接庫。 在工程中引用,只需要include/json及.lib文件即可。
使用JsonCpp前先來熟悉幾個主要的類:
Json::Value 可以表示里所有的類型,比如int,string,object,array等,具體應用將會在后邊示例中介紹。
Json::Reader 將json文件流或字符串解析到Json::Value, 主要函數有Parse。
Json::Writer 與Json::Reader相反,將Json::Value轉化成字符串流,注意它的兩個子類:Json::FastWriter和Json::StyleWriter,分別輸出不帶格式的json和帶格式的json。
1. 從字符串解析json
int ParseJsonFromString()
{
const char* str = "{\"uploadid\": \"UP000000\",\"code\": 100,\"msg\": \"\",\"files\": \"\"}";
Json::Reader reader;
Json::Value root;
if (reader.parse(str, root)) // reader將Json字符串解析到root,root將包含Json里所有子元素
{
std::string upload_id = root["uploadid"].asString(); // 訪問節點,upload_id = "UP000000"
int code = root["code"].asInt(); // 訪問節點,code = 100
}
return 0;
}
2. 從文件解析json
{
"uploadid": "UP000000",
"code": "0",
"msg": "",
"files":
[
{
"code": "0",
"msg": "",
"filename": "1D_16-35_1.jpg",
"filesize": "196690",
"width": "1024",
"height": "682",
"images":
[
{
"url": "fmn061/20111118",
"type": "large",
"width": "720",
"height": "479"
},
{
"url": "fmn061/20111118",
"type": "main",
"width": "200",
"height": "133"
}
]
}
]
}
解析代碼:
int ParseJsonFromFile(
const char* filename)
{
// 解析json用Json::Reader
Json::Reader reader;
// Json::Value是一種很重要的類型,可以代表任意類型。如int, string, object, array
Json::Value root;
std::ifstream
is;
is.open (filename, std::ios::binary );
if (reader.parse(
is, root))
{
std::
string code;
if (!root["files"].isNull())
// 訪問節點,Access an object value by name, create a null member if it does not exist.
code = root["uploadid"].asString();
// 訪問節點,Return the member named key if it exist, defaultValue otherwise.
code = root.
get("uploadid", "null").asString();
// 得到"files"的數組個數
int file_size = root["files"].size();
// 遍歷數組
for(
int i = 0; i < file_size; ++i)
{
Json::Value val_image = root["files"][i]["images"];
int image_size = val_image.size();
for(
int j = 0; j < image_size; ++j)
{
std::
string type = val_image[j]["type"].asString();
std::
string url = val_image[j]["url"].asString();
}
}
}
is.close();
return 0;
}
3. 在json結構中插入json
Json::Value arrayObj; // 構建對象
Json::Value new_item, new_item1;
new_item["date"] = "2011-12-28";
new_item1["time"] = "22:30:36";
arrayObj.append(new_item); // 插入數組成員
arrayObj.append(new_item1); // 插入數組成員
int file_size = root["files"].size();
for(int i = 0; i < file_size; ++i)
root["files"][i]["exifs"] = arrayObj; // 插入原json中
4. 輸出json
// 轉換為字符串(帶格式)
std::string out = root.toStyledString();
// 輸出無格式json字符串
Json::FastWriter writer;
std::string out2 = writer.write(root);
二. 使用Boost property_tree解析json
property_tree可以解析xml,json,ini,info等格式的數據,用property_tree解析這幾種格式使用方法很相似。
解析json很簡單,命名空間為boost::property_tree,reson_json函數將文件流、字符串解析到ptree,write_json將ptree輸出為字符串或文件流。其余的都是對ptree的操作。
解析json需要加頭文件:
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
1. 解析json
解析一段下面的數據:
{
"code": 0,
"images":
[
{
"url": "fmn057/20111221/1130/head_kJoO_05d9000251de125c.jpg"
},
{
"url": "fmn057/20111221/1130/original_kJoO_05d9000251de125c.jpg"
}
]
}
int ParseJson()
{
std::string str = "{\"code\":0,\"images\":[{\"url\":\"fmn057/20111221/1130/head_kJoO_05d9000251de125c.jpg\"},{\"url\":\"fmn057/20111221/1130/original_kJoO_05d9000251de125c.jpg\"}]}";
using namespace boost::property_tree;
std::stringstream ss(str);
ptree pt;
try{
read_json(ss, pt);
}
catch(ptree_error & e) {
return 1;
}
try{
int code = pt.get<int>("code"); // 得到"code"的value
ptree image_array = pt.get_child("images"); // get_child得到數組對象
// 遍歷數組
BOOST_FOREACH(boost::property_tree::ptree::value_type &v, image_array)
{
std::stringstream s;
write_json(s, v.second);
std::string image_item = s.str();
}
}
catch (ptree_error & e)
{
return 2;
}
return 0;
}
2. 構造json
int InsertJson()
{
std::string str = "{\"code\":0,\"images\":[{\"url\":\"fmn057/20111221/1130/head_kJoO_05d9000251de125c.jpg\"},{\"url\":\"fmn057/20111221/1130/original_kJoO_05d9000251de125c.jpg\"}]}";
using namespace boost::property_tree;
std::stringstream ss(str);
ptree pt;
try{
read_json(ss, pt);
}
catch(ptree_error & e) {
return 1;
}
// 修改/增加一個key-value,key不存在則增加
pt.put("upid", "00001");
// 插入一個數組
ptree exif_array;
ptree array1, array2, array3;
array1.put("Make", "NIKON");
array2.put("DateTime", "2011:05:31 06:47:09");
array3.put("Software", "Ver.1.01");
exif_array.push_back(std::make_pair("", array1));
exif_array.push_back(std::make_pair("", array2));
exif_array.push_back(std::make_pair("", array3));
// exif_array.push_back(std::make_pair("Make", "NIKON"));
// exif_array.push_back(std::make_pair("DateTime", "2011:05:31 06:47:09"));
// exif_array.push_back(std::make_pair("Software", "Ver.1.01"));
pt.put_child("exifs", exif_array);
std::stringstream s2;
write_json(s2, pt);
std::string outstr = s2.str();
return 0;
}
三. 兩種解析庫的使用經驗
1. 用boost::property_tree解析字符串遇到"\/"時解析失敗,而jsoncpp可以解析成功,要知道'/'前面加一個'\'是JSON標準格式。
2. boost::property_tree的read_json和write_json在多線程中使用會引起崩潰。
針對1,可以在使用boost::property_tree解析前寫個函數去掉"\/"中的'\',針對2,在多線程中同步一下可以解決。
我的使用心得:使用boost::property_tree不僅可以解析json,還可以解析xml,info等格式的數據。對于解析json,使用boost::property_tree解析還可以忍受,但解析xml,由于遇到問題太多只能換其它庫了。
2014年3月6日
#
上一篇文章,我介紹了KMP算法。
但是,它并不是效率最高的算法,實際采用并不多。各種文本編輯器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法。

Boyer-Moore算法不僅效率高,而且構思巧妙,容易理解。1977年,德克薩斯大學的Robert S. Boyer教授和J Strother Moore教授發明了這種算法。
下面,我根據Moore教授自己的例子來解釋這種算法。
1.

假定字符串為"HERE IS A SIMPLE EXAMPLE",搜索詞為"EXAMPLE"。
2.

首先,"字符串"與"搜索詞"頭部對齊,從尾部開始比較。
這是一個很聰明的想法,因為如果尾部字符不匹配,那么只要一次比較,就可以知道前7個字符(整體上)肯定不是要找的結果。
我們看到,"S"與"E"不匹配。這時,"S"就被稱為"壞字符"(bad character),即不匹配的字符。我們還發現,"S"不包含在搜索詞"EXAMPLE"之中,這意味著可以把搜索詞直接移到"S"的后一位。
3.

依然從尾部開始比較,發現"P"與"E"不匹配,所以"P"是"壞字符"。但是,"P"包含在搜索詞"EXAMPLE"之中。所以,將搜索詞后移兩位,兩個"P"對齊。
4.

我們由此總結出"壞字符規則":
后移位數 = 壞字符的位置 - 搜索詞中的上一次出現位置
如果"壞字符"不包含在搜索詞之中,則上一次出現位置為 -1。
以"P"為例,它作為"壞字符",出現在搜索詞的第6位(從0開始編號),在搜索詞中的上一次出現位置為4,所以后移 6 - 4 = 2位。再以前面第二步的"S"為例,它出現在第6位,上一次出現位置是 -1(即未出現),則整個搜索詞后移 6 - (-1) = 7位。
5.

依然從尾部開始比較,"E"與"E"匹配。
6.

比較前面一位,"LE"與"LE"匹配。
7.

比較前面一位,"PLE"與"PLE"匹配。
8.

比較前面一位,"MPLE"與"MPLE"匹配。我們把這種情況稱為"好后綴"(good suffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后綴。
9.

比較前一位,發現"I"與"A"不匹配。所以,"I"是"壞字符"。
10.

根據"壞字符規則",此時搜索詞應該后移 2 - (-1)= 3 位。問題是,此時有沒有更好的移法?
11.

我們知道,此時存在"好后綴"。所以,可以采用"好后綴規則":
后移位數 = 好后綴的位置 - 搜索詞中的上一次出現位置
舉例來說,如果字符串"ABCDAB"的后一個"AB"是"好后綴"。那么它的位置是5(從0開始計算,取最后的"B"的值),在"搜索詞中的上一次出現位置"是1(第一個"B"的位置),所以后移 5 - 1 = 4位,前一個"AB"移到后一個"AB"的位置。
再舉一個例子,如果字符串"ABCDEF"的"EF"是好后綴,則"EF"的位置是5 ,上一次出現的位置是 -1(即未出現),所以后移 5 - (-1) = 6位,即整個字符串移到"F"的后一位。
這個規則有三個注意點:
?。?)"好后綴"的位置以最后一個字符為準。假定"ABCDEF"的"EF"是好后綴,則它的位置以"F"為準,即5(從0開始計算)。
(2)如果"好后綴"在搜索詞中只出現一次,則它的上一次出現位置為 -1。比如,"EF"在"ABCDEF"之中只出現一次,則它的上一次出現位置為-1(即未出現)。
?。?)如果"好后綴"有多個,則除了最長的那個"好后綴",其他"好后綴"的上一次出現位置必須在頭部。比如,假定"BABCDAB"的"好后綴"是"DAB"、"AB"、"B",請問這時"好后綴"的上一次出現位置是什么?回答是,此時采用的好后綴是"B",它的上一次出現位置是頭部,即第0位。這個規則也可以這樣表達:如果最長的那個"好后綴"只出現一次,則可以把搜索詞改寫成如下形式進行位置計算"(DA)BABCDAB",即虛擬加入最前面的"DA"。
回到上文的這個例子。此時,所有的"好后綴"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"還出現在頭部,所以后移 6 - 0 = 6位。
12.

可以看到,"壞字符規則"只能移3位,"好后綴規則"可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移這兩個規則之中的較大值。
更巧妙的是,這兩個規則的移動位數,只與搜索詞有關,與原字符串無關。因此,可以預先計算生成《壞字符規則表》和《好后綴規則表》。使用時,只要查表比較一下就可以了。
13.

繼續從尾部開始比較,"P"與"E"不匹配,因此"P"是"壞字符"。根據"壞字符規則",后移 6 - 4 = 2位。
14.

從尾部開始逐位比較,發現全部匹配,于是搜索結束。如果還要繼續查找(即找出全部匹配),則根據"好后綴規則",后移 6 - 0 = 6位,即頭部的"E"移到尾部的"E"的位置。
字符串匹配是計算機的基本任務之一。
舉例來說,有一個字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一個字符串"ABCDABD"?

許多算法可以完成這個任務,Knuth-Morris-Pratt算法(簡稱KMP)是最常用的之一。它以三個發明者命名,起頭的那個K就是著名科學家Donald Knuth。

這種算法不太容易理解,網上有很多解釋,但讀起來都很費勁。直到讀到Jake Boxer的文章,我才真正理解這種算法。下面,我用自己的語言,試圖寫一篇比較好懂的KMP算法解釋。
1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一個字符與搜索詞"ABCDABD"的第一個字符,進行比較。因為B與A不匹配,所以搜索詞后移一位。
2.

因為B與A不匹配,搜索詞再往后移。
3.

就這樣,直到字符串有一個字符,與搜索詞的第一個字符相同為止。
4.

接著比較字符串和搜索詞的下一個字符,還是相同。
5.

直到字符串有一個字符,與搜索詞對應的字符不相同為止。
6.

這時,最自然的反應是,將搜索詞整個后移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把"搜索位置"移到已經比較過的位置,重比一遍。
7.

一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。
8.

怎么做到這一點呢?可以針對搜索詞,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,后面再介紹,這里只要會用就可以了。
9.

已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最后一個匹配字符B對應的"部分匹配值"為2,因此按照下面的公式算出向后移動的位數:
移動位數 = 已匹配的字符數 - 對應的部分匹配值
因為 6 - 2 等于4,所以將搜索詞向后移動4位。
10.

因為空格與C不匹配,搜索詞還要繼續往后移。這時,已匹配的字符數為2("AB"),對應的"部分匹配值"為0。所以,移動位數 = 2 - 0,結果為 2,于是將搜索詞向后移2位。
11.

因為空格與A不匹配,繼續后移一位。
12.

逐位比較,直到發現C與D不匹配。于是,移動位數 = 6 - 2,繼續將搜索詞向后移動4位。
13.

逐位比較,直到搜索詞的最后一位,發現完全匹配,于是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向后移動7位,這里就不再重復了。
14.

下面介紹《部分匹配表》是如何產生的。
首先,要了解兩個概念:"前綴"和"后綴"。 "前綴"指除了最后一個字符以外,一個字符串的全部頭部組合;"后綴"指除了第一個字符以外,一個字符串的全部尾部組合。
15.

"部分匹配值"就是"前綴"和"后綴"的最長的共有元素的長度。以"ABCDABD"為例,
?。?A"的前綴和后綴都為空集,共有元素的長度為0;
?。?AB"的前綴為[A],后綴為[B],共有元素的長度為0;
- "ABC"的前綴為[A, AB],后綴為[BC, C],共有元素的長度0;
?。?ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為0;
?。?ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
?。?ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
?。?ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
16.

"部分匹配"的實質是,有時候,字符串頭部和尾部會有重復。比如,"ABCDAB"之中有兩個"AB",那么它的"部分匹配值"就是2("AB"的長度)。搜索詞移動的時候,第一個"AB"向后移動4位(字符串長度-部分匹配值),就可以來到第二個"AB"的位置。
進程(process)和線程(thread)是操作系統的基本概念,但是它們比較抽象,不容易掌握。
最近,我讀到一篇材料,發現有一個很好的類比,可以把它們解釋地清晰易懂。
1.

計算機的核心是CPU,它承擔了所有的計算任務。它就像一座工廠,時刻在運行。
2.

假定工廠的電力有限,一次只能供給一個車間使用。也就是說,一個車間開工的時候,其他車間都必須停工。背后的含義就是,單個CPU一次只能運行一個任務。
3.

進程就好比工廠的車間,它代表CPU所能處理的單個任務。任一時刻,CPU總是運行一個進程,其他進程處于非運行狀態。
4.

一個車間里,可以有很多工人。他們協同完成一個任務。
5.

線程就好比車間里的工人。一個進程可以包括多個線程。
6.

車間的空間是工人們共享的,比如許多房間是每個工人都可以進出的。這象征一個進程的內存空間是共享的,每個線程都可以使用這些共享內存。
7.

可是,每間房間的大小不同,有些房間最多只能容納一個人,比如廁所。里面有人的時候,其他人就不能進去了。這代表一個線程使用某些共享內存時,其他線程必須等它結束,才能使用這一塊內存。
8.

一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,后到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。
9.

還有些房間,可以同時容納n個人,比如廚房。也就是說,如果人數大于n,多出來的人只能在外面等著。這好比某些內存區域,只能供給固定數目的線程使用。
10.

這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。后到的人發現鑰匙架空了,就知道必須在門口排隊等著了。這種做法叫做"信號量"(Semaphore),用來保證多個線程不會互相沖突。
不難看出,mutex是semaphore的一種特殊情況(n=1時)。也就是說,完全可以用后者替代前者。但是,因為mutex較為簡單,且效率高,所以在必須保證資源獨占的情況下,還是采用這種設計。
11.

操作系統的設計,因此可以歸結為三點:
(1)以多進程形式,允許多個任務同時運行;
(2)以多線程形式,允許單個任務分成不同的部分運行;
(3)提供協調機制,一方面防止進程之間和線程之間產生沖突,另一方面允許進程之間和線程之間共享資源。
昨天,我在isnowfy的網站看到,還有其他兩種方法也很簡單,這里做一些筆記。

一、顏色分布法
每張圖片都可以生成顏色分布的直方圖(color histogram)。如果兩張圖片的直方圖很接近,就可以認為它們很相似。

任何一種顏色都是由紅綠藍三原色(RGB)構成的,所以上圖共有4張直方圖(三原色直方圖 + 最后合成的直方圖)。
如果每種原色都可以取256個值,那么整個顏色空間共有1600萬種顏色(256的三次方)。針對這1600萬種顏色比較直方圖,計算量實在太大了,因此需要采用簡化方法??梢詫?~255分成四個區:0~63為第0區,64~127為第1區,128~191為第2區,192~255為第3區。這意味著紅綠藍分別有4個區,總共可以構成64種組合(4的3次方)。
任何一種顏色必然屬于這64種組合中的一種,這樣就可以統計每一種組合包含的像素數量。

上圖是某張圖片的顏色分布表,將表中最后一欄提取出來,組成一個64維向量(7414, 230, 0, 0, 8, ..., 109, 0, 0, 3415, 53929)。這個向量就是這張圖片的特征值或者叫"指紋"。
于是,尋找相似圖片就變成了找出與其最相似的向量。這可以用皮爾遜相關系數或者余弦相似度算出。
二、內容特征法
除了顏色構成,還可以從比較圖片內容的相似性入手。
首先,將原圖轉成一張較小的灰度圖片,假定為50x50像素。然后,確定一個閾值,將灰度圖片轉成黑白圖片。

如果兩張圖片很相似,它們的黑白輪廓應該是相近的。于是,問題就變成了,第一步如何確定一個合理的閾值,正確呈現照片中的輪廓?
顯然,前景色與背景色反差越大,輪廓就越明顯。這意味著,如果我們找到一個值,可以使得前景色和背景色各自的"類內差異最小"(minimizing the intra-class variance),或者"類間差異最大"(maximizing the inter-class variance),那么這個值就是理想的閾值。
1979年,日本學者大津展之證明了,"類內差異最小"與"類間差異最大"是同一件事,即對應同一個閾值。他提出一種簡單的算法,可以求出這個閾值,這被稱為"大津法"(Otsu's method)。下面就是他的計算方法。
假定一張圖片共有n個像素,其中灰度值小于閾值的像素為 n1 個,大于等于閾值的像素為 n2 個( n1 + n2 = n )。w1 和 w2 表示這兩種像素各自的比重。
w1 = n1 / n
w2 = n2 / n
再假定,所有灰度值小于閾值的像素的平均值和方差分別為 μ1 和 σ1,所有灰度值大于等于閾值的像素的平均值和方差分別為 μ2 和 σ2。于是,可以得到
類內差異 = w1(σ1的平方) + w2(σ2的平方)
類間差異 = w1w2(μ1-μ2)^2
可以證明,這兩個式子是等價的:得到"類內差異"的最小值,等同于得到"類間差異"的最大值。不過,從計算難度看,后者的計算要容易一些。
下一步用"窮舉法",將閾值從灰度的最低值到最高值,依次取一遍,分別代入上面的算式。使得"類內差異最小"或"類間差異最大"的那個值,就是最終的閾值。具體的實例和Java算法,請看這里。

有了50x50像素的黑白縮略圖,就等于有了一個50x50的0-1矩陣。矩陣的每個值對應原圖的一個像素,0表示黑色,1表示白色。這個矩陣就是一張圖片的特征矩陣。
兩個特征矩陣的不同之處越少,就代表兩張圖片越相似。這可以用"異或運算"實現(即兩個值之中只有一個為1,則運算結果為1,否則運算結果為0)。對不同圖片的特征矩陣進行"異或運算",結果中的1越少,就是越相似的圖片。
上個月,Google把"相似圖片搜索"正式放上了首頁。
你可以用一張圖片,搜索互聯網上所有與它相似的圖片。點擊搜索框中照相機的圖標。

一個對話框會出現。

你輸入網片的網址,或者直接上傳圖片,Google就會找出與其相似的圖片。下面這張圖片是美國女演員Alyson Hannigan。

上傳后,Google返回如下結果:

類似的"相似圖片搜索引擎"還有不少,TinEye甚至可以找出照片的拍攝背景。

==========================================================
這種技術的原理是什么?計算機怎么知道兩張圖片相似呢?
根據Neal Krawetz博士的解釋,原理非常簡單易懂。我們可以用一個快速算法,就達到基本的效果。
這里的關鍵技術叫做"感知哈希算法"(Perceptual hash algorithm),它的作用是對每張圖片生成一個"指紋"(fingerprint)字符串,然后比較不同圖片的指紋。結果越接近,就說明圖片越相似。
下面是一個最簡單的實現:
第一步,縮小尺寸。
將圖片縮小到8x8的尺寸,總共64個像素。這一步的作用是去除圖片的細節,只保留結構、明暗等基本信息,摒棄不同尺寸、比例帶來的圖片差異。

第二步,簡化色彩。
將縮小后的圖片,轉為64級灰度。也就是說,所有像素點總共只有64種顏色。
第三步,計算平均值。
計算所有64個像素的灰度平均值。
第四步,比較像素的灰度。
將每個像素的灰度,與平均值進行比較。大于或等于平均值,記為1;小于平均值,記為0。
第五步,計算哈希值。
將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖片的指紋。組合的次序并不重要,只要保證所有圖片都采用同樣次序就行了。
=
= 8f373714acfcf4d0
得到指紋以后,就可以對比不同的圖片,看看64位中有多少位是不一樣的。在理論上,這等同于計算"漢明距離"(Hamming distance)。如果不相同的數據位不超過5,就說明兩張圖片很相似;如果大于10,就說明這是兩張不同的圖片。
具體的代碼實現,可以參見Wote用python語言寫的imgHash.py。代碼很短,只有53行。使用的時候,第一個參數是基準圖片,第二個參數是用來比較的其他圖片所在的目錄,返回結果是兩張圖片之間不相同的數據位數量(漢明距離)。
這種算法的優點是簡單快速,不受圖片大小縮放的影響,缺點是圖片的內容不能變更。如果在圖片上加幾個文字,它就認不出來了。所以,它的最佳用途是根據縮略圖,找出原圖。
實際應用中,往往采用更強大的pHash算法和SIFT算法,它們能夠識別圖片的變形。只要變形程度不超過25%,它們就能匹配原圖。這些算法雖然更復雜,但是原理與上面的簡便算法是一樣的,就是先將圖片轉化成Hash字符串,然后再進行比較。
有時候,很簡單的數學方法,就可以完成很復雜的任務。
這個系列的前兩部分就是很好的例子。僅僅依靠統計詞頻,就能找出關鍵詞和相似文章。雖然它們算不上效果最好的方法,但肯定是最簡便易行的方法。
今天,依然繼續這個主題。討論如何通過詞頻,對文章進行自動摘要(Automatic summarization)。

如果能從3000字的文章,提煉出150字的摘要,就可以為讀者節省大量閱讀時間。由人完成的摘要叫"人工摘要",由機器完成的就叫"自動摘要"。許多網站都需要它,比如論文網站、新聞網站、搜索引擎等等。2007年,美國學者的論文《A Survey on Automatic Text Summarization》(Dipanjan Das, Andre F.T. Martins, 2007)總結了目前的自動摘要算法。其中,很重要的一種就是詞頻統計。
這種方法最早出自1958年的IBM公司科學家H.P. Luhn的論文《The Automatic Creation of Literature Abstracts》。
Luhn博士認為,文章的信息都包含在句子中,有些句子包含的信息多,有些句子包含的信息少。"自動摘要"就是要找出那些包含信息最多的句子。
句子的信息量用"關鍵詞"來衡量。如果包含的關鍵詞越多,就說明這個句子越重要。Luhn提出用"簇"(cluster)表示關鍵詞的聚集。所謂"簇"就是包含多個關鍵詞的句子片段。

上圖就是Luhn原始論文的插圖,被框起來的部分就是一個"簇"。只要關鍵詞之間的距離小于"門檻值",它們就被認為處于同一個簇之中。Luhn建議的門檻值是4或5。也就是說,如果兩個關鍵詞之間有5個以上的其他詞,就可以把這兩個關鍵詞分在兩個簇。
下一步,對于每個簇,都計算它的重要性分值。

以前圖為例,其中的簇一共有7個詞,其中4個是關鍵詞。因此,它的重要性分值等于 ( 4 x 4 ) / 7 = 2.3。
然后,找出包含分值最高的簇的句子(比如5句),把它們合在一起,就構成了這篇文章的自動摘要。具體實現可以參見《Mining the Social Web: Analyzing Data from Facebook, Twitter, LinkedIn, and Other Social Media Sites》(O'Reilly, 2011)一書的第8章,python代碼見github。
Luhn的這種算法后來被簡化,不再區分"簇",只考慮句子包含的關鍵詞。下面就是一個例子(采用偽碼表示),只考慮關鍵詞首先出現的句子。
Summarizer(originalText, maxSummarySize):
// 計算原始文本的詞頻,生成一個數組,比如[(10,'the'), (3,'language'), (8,'code')...]
wordFrequences = getWordCounts(originalText)
// 過濾掉停用詞,數組變成[(3, 'language'), (8, 'code')...]
contentWordFrequences = filtStopWords(wordFrequences)
// 按照詞頻進行排序,數組變成['code', 'language'...]
contentWordsSortbyFreq = sortByFreqThenDropFreq(contentWordFrequences)
// 將文章分成句子
sentences = getSentences(originalText)
// 選擇關鍵詞首先出現的句子
setSummarySentences = {}
foreach word in contentWordsSortbyFreq:
firstMatchingSentence = search(sentences, word)
setSummarySentences.add(firstMatchingSentence)
if setSummarySentences.size() = maxSummarySize:
break
// 將選中的句子按照出現順序,組成摘要
summary = ""
foreach sentence in sentences:
if sentence in setSummarySentences:
summary = summary + " " + sentence
return summary
類似的算法已經被寫成了工具,比如基于Java的Classifier4J庫的SimpleSummariser模塊、基于C語言的OTS庫、以及基于classifier4J的C#實現和python實現。