Trie樹的JS或TS實現(xiàn)

數(shù)據(jù)結(jié)構(gòu)在各大開發(fā)語言中應(yīng)用廣泛吓笙,尤其是后端開發(fā)對數(shù)據(jù)的處理映屋,而大部分前端開發(fā)很少應(yīng)用到,或是應(yīng)用場景不允許等因素袱讹,但是了解數(shù)據(jù)結(jié)構(gòu)卻對程序開發(fā)都是有很大裨益的疲扎,那么接下來介紹Trie樹的前端實現(xiàn)。

Trie的簡介

Trie樹捷雕,簡稱“字典樹或前綴樹”,可以存儲字符串與值的對應(yīng)關(guān)系救巷,它與 Java 的 HashMap 功能相同,以 key-value 形式存儲浦译,Trie樹的key是單個字符棒假。

Trie的數(shù)據(jù)結(jié)構(gòu)特點

  • 根節(jié)點不包含字符精盅,除根節(jié)點意外每個節(jié)點只包含一個字符。
  • 從根節(jié)點到某一個節(jié)點渤弛,路徑上經(jīng)過的字符連接起來,為該節(jié)點對應(yīng)的字符串。
  • 每個節(jié)點的所有子節(jié)點包含的字符串不相同鹰贵。
  • 如果字符的種數(shù)為n,則每個結(jié)點的出度為n碉输,這也是空間換時間的體現(xiàn),浪費了很多的空間亭珍。
  • 插入查找的復(fù)雜度為O(n)敷钾,n為字符串長度

如圖,跟節(jié)點是root肄梨,沒有存儲任何字符阻荒,它的key為空,包含了一個數(shù)組存放子節(jié)點众羡。


Trie樹的數(shù)據(jù)結(jié)構(gòu)

Trie樹的操作

  • 插入節(jié)點
    向樹中存儲一個單詞侨赡,比如high,首先從root開始粱侣,遍歷子節(jié)點從high的第一個字符h開始比較羊壹,如果找到了對應(yīng)的子節(jié)點,則遞歸遍歷子節(jié)點的children繼續(xù)查找齐婴,并與單詞的下一個字符(i)比較油猫,直到最后一個字符h為止,如果能找到最后一個字符則說明已存在單詞前綴柠偶;如果過程中未找到則取出字符進行插入到當(dāng)前節(jié)點中情妖,直到單詞的最后一個字符。
  • 查找節(jié)點
    查找節(jié)點的過程嚣州,其實跟插入節(jié)點類似鲫售,也是遍歷+遞歸判斷樹節(jié)點查找單詞是否存在共螺,其滿足條件:單詞的最后一個字符所在節(jié)點是葉子節(jié)點该肴。
  • 刪除節(jié)點
    刪除一個單詞,一般在應(yīng)用中很少操作藐不,但也只需要刪除節(jié)點即可匀哄,先查找到節(jié)點判斷是否存在,如果存在雏蛮,則從單詞倒序遞歸判斷字符是否是葉子節(jié)點涎嚼,如果是則刪除,直到不是葉子節(jié)點結(jié)束挑秉。

原生JS代碼實現(xiàn)

一法梯、定義數(shù)據(jù)結(jié)構(gòu)

Trie有一個根節(jié)點root,它的key為null。

/**
 * Trie
 */
function Trie() {
    this.root = new TrieNode(null);
}

TrieNode有兩個屬性立哑,其中key代表一個字符夜惭,children數(shù)組表示子節(jié)點。

/**
 * 節(jié)點
 * @param {*} key 
 */
function TrieNode(key) {
    this.key = key; // 節(jié)點字符
    this.children = []; // 子節(jié)點集合
}

二铛绰、實現(xiàn)Trie的插入诈茧、查找敢会、刪除鸥昏、輸出

為了更好的描述具體實現(xiàn)細節(jié),以下先聲明列出我要擴展的方法類型互广。接下來將一步一步去講解和實現(xiàn)每個方法及細節(jié)惫皱。

Trie.prototype = {
    // 插入單詞
    insertData:(stringData)=>void,
    insert:(stringData,node)=>void,
    // 查找單詞
    search:(queryData)=>boolean,
    searchNext:(node,stringData)=>boolean, // 遞歸
    // 刪除單詞
    delete:(stringData)=>this,
    delNext:(parent, index, stringData, delStr)=>boolean, // 遞歸
    // 打印樹上的所有單詞
    printData:()=>void,
    printHelper:(node, data)=>void // 遞歸
}

插入單詞

1旅敷、從根節(jié)點開始遍歷樹節(jié)點颤霎,將節(jié)點的key的值與字符串第一個字符比較友酱;
2、如果找到了字符锤躁,則截取剩余子字符串和當(dāng)前節(jié)點繼續(xù)遞歸或详;
3、如果沒有找到字符椒振,則判斷當(dāng)前節(jié)點是否存在子節(jié)點澎迎,若不存在則直接插入,如果存在子節(jié)點辑莫,則遍歷子節(jié)點取出字符與當(dāng)前字符判斷排序位置罩引,最后在該位置插入節(jié)點袁铐;
4、直到字符串最后一個字符屉更,即可完成整個單詞的插入洒缀。

    insertData: function (stringData) {
        this.insert(stringData, this.root);
    },
    // 遞歸判斷插入
    insert: function (stringData, node) {
        if (stringData == '') {
            return;
        }
        let children = node.children;
        let haveData = null;
        for (let i in children) {
            if (children[i].key == stringData[0]) {
                haveData = children[i];
            }
        }
        if (haveData) {
            this.insert(stringData.substring(1), haveData); //說明找到了對應(yīng)的節(jié)點
        } else { //那如果沒有找到則插入
            if (children.length == 0) { //當(dāng)前節(jié)點沒有子節(jié)點
                let node = new TrieNode(stringData[0]);
                children.push(node);
                this.insert(stringData.substring(1), node); //將該字符節(jié)點插入節(jié)點的children中
            } else { //當(dāng)前節(jié)點存在子節(jié)點,需要查找一個合適的位置去插入新節(jié)點
                let validPosition = 0;
                for (let j in children) {
                    if (children[j].key < stringData[0]) {
                        validPosition++;
                    }
                }
                let node = new TrieNode(stringData[0]);
                children.splice(validPosition, 0, node);
                this.insert(stringData.substring(1), node); //將該字符節(jié)點插入節(jié)點的children中
            }
        }
    },

查找單詞萨脑,判斷是否存在

遍歷遞歸邏輯類似渤早,請查看代碼具體注釋鹊杖,需要注意的是調(diào)用遞歸函數(shù)時別忘了return 函數(shù)返回值扛芽。

    // 查詢字符串
    search: function (queryData) {
        if (queryData == '' || this.root.children.length == 0) {
            return false;
        }
        for (let i in this.root.children) {
            if (this.searchNext(this.root.children[i], queryData)) {
                return true;
            }
        }
        return false;
    },
    // 遞歸查詢判斷
    searchNext: function (node, stringData) {
        // 若字符與節(jié)點key不相等,則不匹配
        if (stringData[0] != node.key) {
            return false;
        } else { // 若與key相等登下,繼續(xù)判斷
            let children = node.children;
            if (children.length == 0 && stringData.length == 1) { // 葉子節(jié)點庐船,最后一個字符,則完全匹配
                return true;
            } else if (children.length > 0 && stringData.length > 1) { // 既不是葉子節(jié)點揩瞪,也不是最后一個字符,則繼續(xù)遞歸查找
                for (let i in children) {
                    if (children[i].key == stringData[1]) {
                        return this.searchNext(children[i], stringData.substring(1)); // 記得return 遞歸函數(shù)壹将,否則獲取的返回值為undefined
                    }
                }
            } else { // C1:葉子節(jié)點毛嫉,C2:最后一個字符承粤;若只滿足其中一個條件,則不匹配
                return false;
            }
        }
    },

刪除單詞

遍歷遞歸邏輯類似辛臊,先判斷單詞是否存在彻舰,不存在則不做處理。與查找隔心、插入不同尚胞,刪除需先找到單詞辐真,然后從單詞字符串反向判斷節(jié)點是否是葉子節(jié)點(如high,那么從h耐床、g撩轰、i昧廷、h倒序遍歷判斷)木柬,如果是則刪除,直到單詞某個字符所處的節(jié)點是葉子節(jié)點為止恶复。

    // 刪除字符串
    delete: function (stringData) {
        if (this.search(stringData)) { // 判斷是否存在該單詞(字符串)
            for (let i in this.root.children) {
                if (this.delNext(this.root, i, stringData, stringData)) {
                    return;
                }
            }
        }
        return this;
    },
    /**
     * 先遞歸查找到字符串的葉子節(jié)點,然后從字符串的葉子節(jié)點逐級向根節(jié)點遞歸刪除葉子節(jié)點副硅,直到刪除字符串
     * @param parent 父節(jié)點
     * @param index 子節(jié)點在父節(jié)點children數(shù)組中的索引位置
     * @param stringData 遞歸遍歷中的字符串
     * @param delStr 調(diào)用delete方法時的原始字符串
     */
    delNext: function (parent, index, stringData, delStr) {
        //當(dāng)前節(jié)點對象
        let node = parent.children[index];
        // 若字符與節(jié)點key不相等恐疲,則不匹配
        if (stringData[0] != node.key) {
            return false;
        } else { // 若與key相等套么,繼續(xù)判斷
            let children = node.children;
            if (children.length == 0 && stringData.length == 1) { // 葉子節(jié)點违诗,最后一個字符诸迟,則完全匹配
                // 刪除葉子節(jié)點,利用父節(jié)點刪除子節(jié)點原理
                parent.children.splice(index, 1);
                // 字符串從尾部移除一個字符后壁公,繼續(xù)遍歷刪除方法
                this.delete(delStr.substring(0, delStr.length - 1));
            } else if (children.length > 0 && stringData.length > 1) { // 既不是葉子節(jié)點紊册,也不是最后一個字符快耿,則繼續(xù)遞歸查找
                for (let i in children) {
                    if (children[i].key == stringData[1]) {
                        return this.delNext(node, i, stringData.substring(1), delStr); // 記得return 遞歸函數(shù),否則獲取的返回值為undefined
                    }
                }
            }
        }
    },

輸出所有的單詞

從根節(jié)點開始遍歷掀亥,console.log輸出所有單詞搪花。遞歸單個節(jié)點直到葉子節(jié)點撮竿,輸出單詞,注意data.pop()髓需,遞歸完畢找到葉子節(jié)點后惑折,此操作目的返回原始遍歷節(jié)點繼續(xù)遍歷直到找到下一個單詞為止惨驶。

    // 打印字符串
    printData: function () {
        for (let i in this.root.children) {
            this.printHelper(this.root.children[i], [this.root.children[i].key]);
        }
    },
    // 遞歸輸出字符串
    printHelper: function (node, data) {
        if (node.children.length == 0) {
            console.log('>', data.join(''));
            return;
        }
        for (let i in node.children) {
            data.push(node.children[i].key);
            this.printHelper(node.children[i], data);
            data.pop(); // 注意,找打一個單詞后屋确,返回下一個初始節(jié)點繼續(xù)遍歷
        }
    }

三攻臀、調(diào)試運行

命令行執(zhí)行:node Trie.js刨啸,查看console輸出結(jié)果:


/**
 * 測試
 */
let trie = new Trie();

trie.insertData('我愛你');
trie.insertData('我愛你中國');
trie.insertData('我愛你寶貝');
trie.insertData('我愛你中原');
trie.insertData('愛你一萬年');
trie.insertData('永遠愛你');
trie.insertData('愛你真的好難');

trie.printData();

// console:
// > 我愛你中原
// > 我愛你中國
// > 我愛你寶貝
// > 永遠愛你
// > 愛你一萬年
// > 愛你真的好難

// console.log(trie.search('我愛你')); // false
// console.log(trie.search('我愛你中國')); // true
// console.log(trie.search('我愛你寶寶')); // false
// console.log(trie.search('我愛你寶貝')); // true

console.log(JSON.stringify(trie.delete('愛你真的好難')));
// 查看輸出發(fā)現(xiàn)识脆,單詞已刪除

上面是原生JS實現(xiàn)灼捂,下面用TypeScript重寫

TypeScript 這門語言越來越流行了悉稠,像主流框架React的猛、Vue、Angular叛拷,都有很好的支持猫牡。重寫主要體現(xiàn)TS的面向?qū)ο筇匦裕谶@里主要用到了TS的接口及實現(xiàn)煌恢、類的繼承瑰抵、模塊器联、類型斷言等婿崭,話不多說了氓栈,直接上代碼:

Trie.ts文件

/**
 * 接口類
 */
interface ITrie {
    /**
     * 插入單詞
     * @param data 
     */
    insertData(data: string): void;
    /**
     * 刪除單詞
     * @param data 
     */
    deleteData(data: string): Trie;
    /**
     * 查找單詞
     * @param data 
     */
    searchData(data: string): boolean;
    /**
     * 輸出單詞列表
     */
    printData(): void;
}
/**
 * 基類
 */
class TrieBase {
    /**
     * 本類不能實例化對象授瘦,能被繼承
     */
    protected constructor() { }

    /**
     * 占個位提完,去派生類中重寫
     * @param stringData 
     */
    protected deleteData(stringData) {
        return
    }

    /**
     * 遞歸插入單詞
     * @param stringData 
     * @param node 
     */
    protected insert(stringData, node) {
        if (stringData == '') {
            return;
        }
        let children = node.children;
        let haveData = null;
        for (let i in children) {
            if (children[i].key == stringData[0]) {
                haveData = children[i];
            }
        }
        if (haveData) {
            this.insert(stringData.substring(1), haveData); //說明找到了對應(yīng)的元素
        } else { //那如果沒有找到
            if (children.length == 0) {
                //當(dāng)前沒有子元素,所以應(yīng)該判斷一下
                let node = new TrieNode(stringData[0]);
                children.push(node);
                this.insert(stringData.substring(1), node); //對吧扳躬,此時應(yīng)該將該元素插入子元素中
            } else { //當(dāng)前子元素的長度不為零,需要查找一個合適的位置去插入元素
                let validPosition = 0;
                for (let j in children) {
                    if (children[j].key < stringData[0]) {
                        validPosition++;
                    }
                }
                let node = new TrieNode(stringData[0]);
                children.splice(validPosition, 0, node);
                this.insert(stringData.substring(1), node); //對吧,此時應(yīng)該將該元素插入子元素中
            }
        }
    }

    /**
     * 先遞歸查找到字符串的葉子節(jié)點担神,然后從字符串的葉子節(jié)點逐級向根節(jié)點遞歸刪除葉子節(jié)點妄讯,直到刪除字符串
     * @param parent 父節(jié)點
     * @param index 子節(jié)點在父節(jié)點children數(shù)組中的索引位置
     * @param stringData 遞歸遍歷中的字符串
     * @param delStr 調(diào)用deleteData方法時的原始字符串
     */
    protected delNext(parent, index, stringData, delStr) {
        //當(dāng)前節(jié)點對象
        let node = parent.children[index];
        // 若字符與節(jié)點key不相等亥贸,則不匹配
        if (stringData[0] != node.key) {
            return false;
        } else { // 若與key相等炕置,繼續(xù)判斷
            let children = node.children;
            if (children.length == 0 && stringData.length == 1) { // 葉子節(jié)點,最后一個字符默垄,則完全匹配
                // 刪除葉子節(jié)點口锭,利用父節(jié)點刪除子節(jié)點原理
                parent.children.splice(index, 1);
                // 字符串從尾部移除一個字符后鹃操,繼續(xù)遍歷刪除方法
                this.deleteData(delStr.substring(0, delStr.length - 1));
            } else if (children.length > 0 && stringData.length > 1) { // 既不是葉子節(jié)點,也不是最后一個字符恩伺,則繼續(xù)遞歸查找
                for (let i in children) {
                    if (children[i].key == stringData[1]) {
                        return this.delNext(node, i, stringData.substring(1), delStr); // 記得return 遞歸函數(shù)莫其,否則獲取的返回值為undefined
                    }
                }
            }
        }
    }

    /**
     * 遞歸查找單詞
     * @param node 
     * @param stringData 
     */
    protected searchNext(node, stringData) {
        // 若字符與節(jié)點key不相等耸三,則不匹配
        if (stringData[0] != node.key) {
            return false;
        } else { // 若與key相等仪壮,繼續(xù)判斷
            let children = node.children;
            if (children.length == 0 && stringData.length == 1) { // 葉子節(jié)點胳徽,最后一個字符养盗,則完全匹配
                return true;
            } else if (children.length > 0 && stringData.length > 1) { // 既不是葉子節(jié)點,也不是最后一個字符箫爷,則繼續(xù)遞歸查找
                for (let i in children) {
                    if (children[i].key == stringData[1]) {
                        return this.searchNext(children[i], stringData.substring(1)); // 記得return 遞歸函數(shù)虎锚,否則獲取的返回值為undefined
                    }
                }
            } else { // C1:葉子節(jié)點窜护,C2:最后一個字符柱徙;若只滿足其中一個條件护侮,則不匹配
                return false;
            }
        }
    }

    /**
     * 遞歸打印單詞
     * @param node 
     * @param data 
     */
    protected printHelper(node, data) {
        if (node.children.length == 0) {
            console.log('>', data.join(''));
            return;
        }
        for (let i in node.children) {
            data.push(node.children[i].key);
            this.printHelper(node.children[i], data);
            data.pop();
        }
    }

    /**
     * 類型保護概行,以免typescript報錯凳忙,或使用類型斷言
     * @param node 
     */
    protected isTrieNode(node: TrieNode): node is TrieNode {
        return (<TrieNode>node).key !== undefined;
    }
}
/**
 * 節(jié)點
 * @param {*} key
 */
class TrieNode {
    key: string;
    children: [];
    constructor(key) {
        this.key = key; // 節(jié)點字符
        this.children = []; // 子節(jié)點集合
    }
}
/**
 * Trie類
 */
class Trie extends TrieBase implements ITrie {
    root: TrieNode;
    constructor() {
        super();
        this.root = new TrieNode(null);
    }
    //插入單詞(字符串)
    insertData(stringData): void {
        this.insert(stringData, this.root);
    }
    //刪除單詞
    deleteData(stringData): Trie {
        if (this.searchData(stringData)) { // 判斷是否存在該單詞(字符串)
            for (let i in this.root.children) {
                if (this.delNext(this.root, i, stringData, stringData)) {
                    return;
                }
            }
        }
        return this;
    }
    //查找單詞(字符串)
    searchData(queryData): boolean {
        if (queryData == '' || this.root.children.length == 0) {
            return false;
        }
        for (let i in this.root.children) {
            if (this.searchNext(this.root.children[i], queryData)) {
                return true;
            }
        }
        return false;
    }
    //輸出所有單詞(字符串)
    printData(): void {
        for (let i in this.root.children) {
            //為了讓代碼工作勤家,第二個參數(shù),我使用了類型斷言柳恐,避免TS編譯報錯伐脖,找不到key屬性,也可以使用類型保護函數(shù)(從TrieBase類繼承過來的isTrieNode函數(shù)判斷)
            this.printHelper(this.root.children[i], [(<TrieNode>this.root.children[i]).key]);
        }
    }
}
// 導(dǎo)出Trie模塊
export { Trie }

在其他模塊中使用Trie模塊

// 導(dǎo)入模塊
import {Trie} from 'Trie';

/**
 * 使用
 */
let trieObj = new Trie();

trieObj.insertData('我愛你');
trieObj.insertData('我愛你中國');
trieObj.insertData('我愛你寶貝');
trieObj.insertData('我愛你中原');
trieObj.insertData('愛你一萬年');
trieObj.insertData('永遠愛你');
trieObj.insertData('愛你真的好難');

trieObj.printData();

// console:
// > 我愛你中原
// > 我愛你中國
// > 我愛你寶貝
// > 永遠愛你
// > 愛你一萬年
// > 愛你真的好難

console.log(trieObj.searchData('我愛你')); // false
console.log(trieObj.searchData('我愛你中國')); // true
console.log(trieObj.searchData('我愛你寶寶')); // false
console.log(trieObj.searchData('我愛你寶貝')); // true

console.log(JSON.stringify(trieObj.deleteData('愛你真的好難')));

運行TS

// 安裝
npm install -g typescript
// 編譯生成Trie.js
tsc Trie.ts 
// 運行
node Trie.js 

好了乐设,講完了讼庇,有點啰嗦莫怪,就寫這么多吧近尚,希望對你有所幫助蠕啄,如不正確的地方也請指正,有問題也可留言或私信郵件戈锻,歡迎交流歼跟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哈街,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子骤竹,更是在濱河造成了極大的恐慌,老刑警劉巖懒震,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件递宅,死亡現(xiàn)場離奇詭異,居然都是意外死亡安接,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門撩银,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抄邀,你說我怎么就攤上這事。” “怎么了环鲤?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵西剥,是天一觀的道長。 經(jīng)常有香客問我图甜,道長黑毅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任易结,我火速辦了婚禮,結(jié)果婚禮上鹦肿,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好霹陡,可當(dāng)我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辅髓,像睡著了一般矫付。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杀赢,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天砌左,我揣著相機與錄音适刀,去河邊找鬼硝皂。 笑死,一個胖子當(dāng)著我的面吹牛吼过,可吹牛的內(nèi)容都是我干的羊赵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后汇鞭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霍骄,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡米间,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年困曙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡局劲,死狀恐怖苹丸,靈堂內(nèi)的尸體忽然破棺而出商模,到底是詐尸還是另有隱情鄙信,我是刑警寧澤杏糙,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布谅河,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏狐粱。R本人自食惡果不足惜蒋搜,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望容贝。 院中可真熱鬧斤富,春花似錦膏潮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轻纪。三九已至,卻和暖如春叠纷,著一層夾襖步出監(jiān)牢的瞬間刻帚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工讲岁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留我擂,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓缓艳,卻偏偏與公主長得像校摩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子阶淘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容