如何處理十萬級別的數據信息?
學習數據結構後,嘗試做一些東西。如果不使用資料庫去寫一個圖書館管理系統,那麼數十萬的圖書信息該如何處理?如果數組超表示,那麼增刪很不方便,如果鏈表表示,排序有不太容易,請問該如何處理?求大神提供思路)
一直想寫一個"二十分鐘平衡樹小教程"
題主大概需要一個 唯一標識(key)-&>書本信息(value) 的映射,簡單的唯一標識可以是一個int類型的數,然後可以通過唯一標識來排序
好吧,實際上就是需要一個map&
map內部用的是紅黑樹,如果題主想手寫一個的話,建議寫一個treap
首先說明一下平衡樹,平衡樹是一顆二叉樹,每個節點有個鍵值,這個鍵值比左兒子的鍵值大,比右兒子的鍵值小。滿足這個條件叫二叉排序樹,平衡樹似乎有些嚴格的定義,我這裡指的是高度在log(節點數)這個級別的二叉排序樹,這樣增刪改查等操作複雜度都是log級別的
古典的treap給每個節點附帶了一個priority, 按key排列成二叉排序樹,按priority排列成堆
這裡想介紹一種我認為更好寫的treap
struct Node {
int key, size;
Node *left, *right;
Node (int key, int size, Node *left, Node *right) :
key(key), size(size), left(left), right(right) {}
Node *update() {
size = left-&>size + 1 + right-&>size;
return this;
}
Pair split(int n);
};
首先是定義,這是樹中一個節點的定義,key代表健值,left和right分別是左右子樹,size代表子樹大小。這裡很簡單明了,就和一顆普通的二叉樹差不多
Node *merge(Node *a, Node *b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
if (ran() % (a-&>size + b-&>size) &< a-&>size) {
a-&>right = merge(a-&>right, b);
return a-&>update();
} else {
b-&>left = merge(a, b-&>left);
return b-&>update();
}
}
typedef pair &
Pair Node :: split(int n) {
if (this == null) {
return make_pair(null, null);
}
if (key &>= n) {
Pair ret = left-&>split(n);
left = ret.second;
return make_pair(ret.first, this-&>update());
} else {
Pair ret = right-&>split(n);
right = ret.first;
return make_pair(this-&>update(), ret.second);
}
}
(這個程序看起來有點長,主要是因為風格問題,實際代碼量極少)
這個treap僅有merge和split兩個操作
merge(a, b)中,要求a的任意節點的鍵值小於b的任意節點的鍵值,函數的返回結果是把a, b兩棵樹合併起來,組成一顆新的treap
我們考慮在古典treap里,priority的最大值落在a樹中的概率是a-&>size / (a-&>size + b-&>size),否則落在b樹中。如果ab都非空,那麼用隨機函數選一個根,並把它的某顆子樹和另一棵樹合併起來即可。這個代碼應該挺直白的,就是不斷遞歸調用merge直到某一棵樹為空。
Node.split(int n)中,會把Node切割成兩棵樹,其中ret.first鍵值全部&
好了,完成這兩個函數,就可以很方便的實現平衡樹上的增刪改查啦!
假設我們有一顆樹,根節點為root
增:如果要增加鍵值為K的一個點X,那麼只需要把root按K切割開,然後依次合併起來即可
(Pair ret = root.split(K); root = merge(merge(root.first, X), root.second)
刪:如果要刪除鍵值為K的點,那麼把root按K切開,切完的second按K + 1切開,這樣鍵值為K的點(如果有)就被切成了單獨的一顆子樹,扔掉就好了
(Pair ret1 = root.split(K), ret2 = ret1.split(K + 1); root = merge(ret1.first, ret2.second))
我們注意到以上程序中,ret2.first就是鍵值K的點構成的樹
改:和上一步類似,對ret2.first做修改,然後root = merge(ret1.first, merge(ret2.first, ret2.second))
查:同上
同理還可以做很多操作.. 比如前驅後繼,或者抽出一段區間Key來干點事情這種.. 我每次寫這個treap都覺得大開大合的非常爽
實際上寫起來的時候,可能會有點細節需要注意,我這裡貼一個完整版的,我大一時候的一個數據結構作業,實現了一個類似java的TreeMap,如果有興趣可以對照著看看
/** @file */
#ifndef __TREEMAP_H
#define __TREEMAP_H
#include "ElementNotExist.h"
#include &
using namespace std;
/**
* TreeMap is the balanced-tree implementation of map. The iterators must
* iterate through the map in the natural order (operator&<) of the key.
*/
template&
class TreeMap
{
private:
static int ran()
{
static int x = 1;
x += (x &<&< 1) + 1;
return x 2147483647;
}
struct Node;
typedef pair &
Node *null;
struct Node
{
K key;
V val;
int size;
Node *left, *right;
Node () {}
Node (int size, Node *left, Node *right) :
size(size), left(left), right(right) {}
Node (K key, V val, int size, Node *left, Node *right) :
key(key), val(val), size(size), left(left), right(right) {}
Node *Update()
{
size = left-&>size + 1 + right-&>size;
return this;
}
const V find2(K x) const
{
if (!(x &< key) !(key &< x))
return val;
if (key &< x)
return right-&>find2(x);
else
return left-&>find2(x);
}
bool find(K x, Node *null) const
{
if (this == null)
return false;
if (!(x &< key) !(key &< x))
return true;
if (key &< x)
return right-&>find(x, null);
else
return left-&>find(x, null);
}
bool findV(V x, Node *null) const
{
if (this == null)
return false;
if (!(x &< val) !(val &< x))
return true;
return left-&>findV(x, null) || right-&>findV(x, null);
}
void getNext(K x, K ans_key, V ans_val, Node *null)
{
if (this == null)
return;
if (x &< key)
{
ans_key = key;
ans_val = val;
left-&>getNext(x, ans_key, ans_val, null);
}
else
right-&>getNext(x, ans_key, ans_val, null);
}
Pair Split(K x, Node *null)
{
if (this == null)
return make_pair(null, null);
if (x &< key)
{
Pair ret = left-&>Split(x, null);
left = ret.second;
return make_pair(ret.first, this-&>Update());
}
Pair ret = right-&>Split(x, null);
right = ret.first;
return make_pair(this-&>Update(), ret.second);
}
Pair SplitLess(K x, Node *null)
{
if (this == null)
return make_pair(null, null);
if (!(key &< x))
{
Pair ret = left-&>SplitLess(x, null);
left = ret.second;
return make_pair(ret.first, this-&>Update());
}
Pair ret = right-&>SplitLess(x, null);
right = ret.first;
return make_pair(this-&>Update(), ret.second);
}
};
void treapClear(Node *now)
{
if (now == null)
return;
treapClear(now-&>left);
treapClear(now-&>right);
delete now;
}
Node *Merge(Node *a, Node *b)
{
if (a == null)
return b;
if (b == null)
return a;
if (ran() % (a-&>size + b-&>size) &< a-&>size)
{
a-&>right = Merge(a-&>right, b);
return a-&>Update();
}
b-&>left = Merge(a, b-&>left);
return b-&>Update();
}
int _size;
public:
Node *_root;
class Entry
{
K key;
V value;
public:
Entry () {}
Entry(K k, V v)
{
key = k;
value = v;
}
const K getKey() const
{
return key;
}
const V getValue() const
{
return value;
}
};
class Iterator
{
private:
int _now;
K _key;
Entry ret;
const TreeMap &
public:
Iterator (int _now, const TreeMap &
_now(_now), _temp(temp), _key(_key) {}
/**
* Returns true if the iteration has more elements.
*/
bool hasNext() {
return _now &< _temp.size();
}
/**
* Returns the next element in the iteration.
* @throw ElementNotExist exception when hasNext() == false
*/
const Entry next() {
if (!hasNext())
throw ElementNotExist();
if (_now == 0)
{
Node *now = _temp._root;
while (now-&>left != _temp.null)
now = now-&>left;
ret = Entry(now-&>key, now-&>val);
_now++;
_key = ret.getKey();
return ret;
}
else
{
K ans_key;
V ans_val;
int tag = 0;
Node *nnow = _temp.null;
_temp._root-&>getNext(_key, ans_key, ans_val, nnow);
_now++;
ret = Entry(ans_key, ans_val);
_key = ans_key;
return ret;
}
}
};
/**
* Constructs an empty tree map.
*/
TreeMap() {
null = new Node(0, null, null);
_size = 0;
_root = null;
}
/**
* Destructor
*/
~TreeMap() {
treapClear(_root);
delete null;
}
/**
* Assignment operator
*/
TreeMap operator=(const TreeMap x) {
if (this == x)
return *this;
treapClear(_root);
_root = null;
_size = 0;
Iterator it = x.iterator();
while (it.hasNext())
{
Entry now = it.next();
put(now.getKey(), now.getValue());
}
return *this;
}
/**
* Copy-constructor
*/
TreeMap(const TreeMap x) {
null = new Node(0, null, null);
_size = 0;
_root = null;
Iterator it = x.iterator();
while (it.hasNext())
{
Entry now = it.next();
put(now.getKey(), now.getValue());
}
}
/**
* Returns an iterator over the elements in this map.
*/
Iterator iterator() const {
K know;
return Iterator(0, *this, know);
}
/**
* Removes all of the mappings from this map.
*/
void clear() {
treapClear(_root);
_root = null;
_size = 0;
}
/**
* Returns true if this map contains a mapping for the specified key.
*/
bool containsKey(const K key) const {
Node *nnow = null;
return _root-&>find(key, nnow);
}
/**
* Returns true if this map maps one or more keys to the specified value.
*/
bool containsValue(const V value) const {
Node *nnow = null;
return _root-&>findV(value, nnow);
}
/**
* Returns a const reference to the value to which the specified key is mapped.
* If the key is not present in this map, this function should throw ElementNotExist exception.
* @throw ElementNotExist
*/
const V get(const K key) const {
Node *nnow = null;
if (!_root-&>find(key, nnow))
throw ElementNotExist();
return _root-&>find2(key);
}
/**
* Returns true if this map contains no key-value mappings.
*/
bool isEmpty() const {
return _size == 0;
}
/**
* Associates the specified value with the specified key in this map.
*/
void put(const K key, const V value) {
Pair ret1 = _root-&>SplitLess(key, null), ret2 = ret1.second-&>Split(key, null);
if (ret2.first-&>size == 0)
{
_size++;
_root = Merge(ret1.first, Merge(new Node(key, value, 1, null, null), ret2.second));
return;
}
ret2.first-&>val = value;
_root = Merge(ret1.first, Merge(ret2.first, ret2.second));
}
/**
* Removes the mapping for the specified key from this map if present.
* If there is no mapping for the specified key, throws ElementNotExist exception.
* @throw ElementNotExist
*/
void remove(const K key) {
Pair ret1 = _root-&>SplitLess(key, null), ret2 = ret1.second-&>Split(key, null);
if (ret2.first-&>size == 0)
{
_root = Merge(ret1.first, ret2.second);
throw ElementNotExist();
}
_size--;
_root = Merge(ret1.first, ret2.second);
delete ret2.first;
}
/**
* Returns the number of key-value mappings in this map.
*/
int size() const {
return _size;
}
};
#endif
才10萬,用std::map就好了,如果想自己寫的話,紅黑樹太麻煩了,改寫平衡二叉樹吧。如果你要多個索引,那也很容易,std::map的key其實就是primary key,然後其他索引就是把其他的值map到primary key上。
你意識不到其實你提了一個很好的問題,事實上商業應用當中的確有這個問題,我mark下,等下有空了來答
雖然你是在練習的時候提出的這個問題,但是事實這個問題在真實世界裡有其存在。下面很多人的回答都基於工程(engineering),我來基於架構(architecture)進行下討論。
現實的狀況,很多時候,並不具備安裝無論是關係型還是NOsql的情況,並非很多系統都擁有無限的後台,我就遇到過現實的要求,在落後地區比如非洲,網路不普及或者不穩定地區比如東南亞的某些小國,在有神經病要求的地區,比如日本鄉村,機器的狀況很差。你見過只有一個單片機要跑金融系統的狀況嗎?只有一個平板要儲存數十萬計算規則的情況嗎?這些都真實存在於這個世界上。這時候,安裝一個資料庫就是非常奢侈的事情,應用能裝進去就很不錯了。有人會說,安卓上的嵌入式資料庫也很不錯,當然還有內存資料庫可以使用,沒錯,是有這些工程(engineering)的做法,但是,現實情況是不允許這麼做,有財務方面的考慮,有數據大小的限制,有單純的人為限制。所以我總是說,先架構,後工程才是正確的做法,相當多的初級中級人員,腦子裡只有工程,沒有架構思想,這就阻止了他更上一層樓,好在技術有一個分支,是chief engineer,能夠單單專心於工程。
好,如果前提條件是就不能使用資料庫,如何組織我們的數據?這要區分兩個概念,運行時數據,和持久化數據。斷電以後能存在的數據,和在計算過程當中的數據,他們應該有不同的存儲方式。比如你完全沒有資料庫,那麼你可能不得不使用文件系統來進行存儲,這一套你可以設計,最簡單就是古代COBOL存儲方式,按行定長。內存中的數據,你既可以採取古典方式,以object的方式存儲使用,也可以用內存資料庫在計算是讀入的方式來組織。你目前遇到的迷思主要在於運行時數據和儲存數據沒有分層考慮。你的系統設計應該有一個業務層,這個層專註於利用運行時數據,其下的一個層乃是運行時數據組織層,這個層用來保存運行時數據,提供給上層使用,再其下,乃是data access object層,這個層專註於讀寫持久化數據到運行時數據層,最下層乃是持久化層。這樣的話, 你在每個層需要處理的任務非常集中。你可能採取數組方式在運行時組織數據,但是你的持久化層的存儲可能採取鏈表(具體可以繼續優化設計,僅僅比喻),這樣你就不會存在數組的持久化會很低效這種問題了。
=========編輯,稍微說一下文件中處理的一些思路==============
文件基本上不會給你什麼旋轉啊之類的操作的資本(不是不可以,是沒必要),所以各種模型在內存中實現就是了。文件里對於不算大的數據量(比如說10萬這個級別),直接序列寫入就好了
重點說一下append write和overwrite三個寫入
append適合往尾部添加數據,看情況選擇就好
write是對於小文件比較方便的操作,適用於需要修改數據的場合,直接把原先了擦了重寫就好了
著重說一下overwrite
overwrite說白了就是把定長的內存模型實現到文件中,換句話說文件中的binary內容也可以看作一個內存塊的內容。
我們先假定你是在c++平台上,有
class Publication{
string name;
string id;
string author;
string publisher;
//...
};
每一個數據都是字元串,然後長度都不會超過120bytes(不含尾部的 )
於是我們可以認為,每一個publication實例在都可以對應一個定長480bytes的內存塊
那麼,就有
Collection&
這個容器在內存中的總長度就是480bytes*size
然後如果要便於操作的話,我們還需要給每一個實例在內存中的起點增加一個識別頭(比如說一個特定的20bytes長度的二進位串),這樣每個實例的長度就是500,而總長就是500*size
接下來,在內存中,我們可以為每個實例增加一個標識符,該數值是其在文件中對應的streampos
class Publication{
string name;
string id;
string author;
string publisher;
streampos offset;
//...
};
然後,我們對於讀寫,就可以用這種形式進行
void Publication::write(ostream out){
char buffer[500];
out.seekp(this-&>offset,ios::beg);
out.write(symbol,20);
memset(buffer,0,500);
memcpy(buffer,symbol,20);
memcpy(buffer+20,id.c_str(),min(120,id.size()));
memcpy(buffer+140,name.c_str(),min(120,name.size()));
memcpy(buffer+260,author.c_str(),min(120,author.size()));
memcpy(buffer+380,publisher.c_str(),min(120,publisher.size()));
out.write(buffer,50);
}
void Publication::read(istream in){
char buffer[500];
char validator[20];
char strbuffer[121];
streampos off = in.tellg();
in.read(buffer,500);
memcpy(validator,buffer+0,20);
for(int i=0;i&<20;i++){
if(validator[i] != symbol[i]){
//validation fail
in.seekg(off);
return;
}
}
offset = off;
memset(strbuffer,0,121);
memcpy(strbuffer,buffer+20,120);
id = strbuffer;
memset(strbuffer,0,121);
memcpy(strbuffer,buffer+140,120);
name = strbuffer;
memset(strbuffer,0,121);
memcpy(strbuffer,buffer+260,120);
author = strbuffer;
memset(strbuffer,0,121);
memcpy(strbuffer,buffer+380,120);
publisher = strbuffer;
}
很自然,對於單獨的處理,只需要打開文件然後找定文件中的位置就好了
=============分割線=============
區區十萬級,目標要盯著騰訊那裡傳說中的並發每秒10億級的才對嘛
嘛開玩笑,提供點思路
std::map的紅黑樹性能還是有限,建議考慮從B* Tree開始,圖書館一般不用考慮刪除,所以B*Tree是個不錯的選擇
然後增加對於Visit Frequency的索引,將高頻訪問保留獨立的入口以提升查詢速度,原則上這裡應該是個Priority Queue,然而因為需要Live updating,所以傳統的Heap結構顯然不合適了,建議的選擇是Skip-List,對於需要快速訪問並重組的場景而言相當的有效,而且可以定期或者不定期清理低訪問量的
再進一步,對於更多的訪問處理,可以使用多線程和緩衝區來組合進行。對於高訪問的目標,創建目標副本並通過線程池來在各自的副本中處理Readonly的訪問請求
如果請求增加Write的要求,那麼就需要使用hash code的版本比對機制。將Write和Read分離,每當Write發生時同步更新code,同時在下次Read請求發生時先行下發newest code,再轉發至cdn用於更新數據並重新下發。
blabla,好像亂七八糟說的多了點
要我的話就結構數組+二分法了(滑稽)當然map/hash都可以,最方便的還是寫個xml/json,然後上VSCode ctrl+F,找到人工讀出來
請用資料庫。如果作為數據結構的練習,可以用鏈表存圖書記錄,用m路平衡樹做索引。
才十萬的話用excel吧,方便高效,還可以配合VBScript
為什麼不用資料庫?
這不自討苦吃嗎?資料庫幫你省了無數的工作。排序查詢索引事務,那一樣不重要?
自己實現不僅問題重重,而且最後還完成不了。你的重點是實現圖書管理系統。那麼趕快把管理的功能完成。光這些功能做好就非常非常不容易。資料庫,一個sql就能解決的的事,不要費很大事自己做然後半天也跑不出來————————————————————不用資料庫請你用哈希表,查找排序比樹簡單一點
你用文本序列化數據 處理十萬級數據
然後發現
需要 添加、修改、刪除、
需要排序、篩選、查找
需要分表、鎖、事務
然後……
你發現自己實現了一個資料庫
還是特別LOW的那種……
看了半天題目,感覺和圖書館管理系統沒關係,題目應該換成10萬數據如何快速增刪改查。
你基礎太差,補好基礎再做吧。
1.這種管理系統數據必須存儲在資料庫,需要持久存儲。
2.這種系統不需要與客戶端實時交互,可以延遲,且用戶之間也沒有實時交互,沒必要把數據都放在內存。
3.你就是想放在內存用來練習的話,那你只提到數組和鏈表,這是查找最慢的,你至少應該知道二叉樹吧,知道二叉樹的原理你就知道怎麼查找快了,更進一步就是多叉樹、紅黑樹之類的。
4.3中提到的這些在c++中都有實現,是容器,比如紅黑樹就是map;如果數據較大,想要更高的效率,那用哈希表unordered_map(但是無序)。
你這上來就要做,做了也沒價值,所以 真的『』學習數據結構後 「再規劃一下怎麼做吧。真的學了不可能在這裡說用數組和鏈表了。
十萬,也就是100k么??這麼點數據量,還有什麼要問的么?
自己寫個資料庫。
用數組 這麼點數據 不是問題 先做東西出來 資料庫後面再學吧
easy 模式:建一千個文件夾,每個數據的主鍵算個hash,對1k取余,放進相應的文件夾里。文件名是hash,內容是記錄。其實就是充分利用文件系統。
hard 模式:自己寫一個k-v資料庫。因為考慮你的需求,不需要隨機訪問,k-v應該也夠了吧。才十萬啊,NTFS目錄不就挺好用,還nosql了。
這個數據級別想怎麼處理就怎麼處理
巧了。。我們今年大作業也是圖書館 我們10w本書基本秒搜
不過思路可能和其他答案不太一樣 因為我沒有使用什麼高級數據結構 就是數組
提前聲明我們圖書館是任何改動都直接寫入到文件 而不是在內存操作最後寫入文件
我覺得重點在於文件的儲存方式 如果你是一本書一個文件 那麼10w基本需要12秒 .net4.5 c# 性能分析瓶頸在於反覆開關文件 如果你把所有的書存到一個文件。。那麼會有一些不定長信息 比如借閱者這些 很麻煩
我的方法是所有書的基本索引信息(設置檢索欄位的信息 比如書號書名作者出版社)存一個文件 這樣基本搜索只需要開一個文件(類似查找表?) 載入書籍詳情再進該書的詳細信息文件
這樣10w秒搜 100w 10秒左右吧 因為你的瓶頸在於文件讀寫 並不在於你用什麼數據結構 你要是全在內存操作 使用紅黑或者b樹可能有用 但要是像我這樣 內存中並不涉及對書籍排序 添加刪除直接寫文件 使用數據結構用處不大
而且個人覺得 既然瓶頸在於讀寫文件 那就應該把讀寫文件操作分散 而不是先在內存搞搞 退出統一寫文件
最後 這是我們數據結構課的課程設計 我和隊友討論覺得使用高級數據結構沒什麼卵用 也不知道是我們native還是咋的。。有大佬有什麼思路意見歡迎指教 萌新也想長見識 謝謝!
等我整理整理在附一個git地址吧 ? ??https://github.com/Kou-Akira/LIBRARYstruct BOOK* books[1000000];
毫無問題...
推薦閱讀:
※一個程序員會遇到多少關於數據結構與演算法的需求?
※有沒有一種數據結構,查找、刪除和插入效率都比較高呢?
※如何在程序中將中綴表達式轉換為後綴表達式?
※如何高效地判斷兩個單鏈表是否有交叉?
※希爾排序對於h有序數組排序的選擇問題?