Python: Trie樹實現字典排序

通常語言都提供了按字典排序的API,好比跟微信公衆平臺對接時就須要用到字典排序。按字典排序有不少種算法,最容易想到的就是字符串搜索的方式,但這種方式實現起來很麻煩,性能也不太好。Trie樹是一種很經常使用的樹結構,它被普遍用於各個方面,好比字符串檢索、中文分詞、求字符串最長公共前綴和字典排序等等,並且在輸入法中也能看到Trie樹的身影。php

什麼是Trie樹

Trie樹一般又稱爲字典樹、單詞查找樹或前綴樹,是一種用於快速檢索的多叉樹結構。如圖數字的字典是一個10叉樹:
node

同理小寫英文字母或大寫英文字母的字典數是一個26叉樹。如上圖可知,Trie樹的根結點是不保存數據的,全部的數據都保存在它的孩子節點中。有字符串go, golang, php, python, perl,它這棵Trie樹可以下圖所示構造:python

咱們來分析下上面這張圖。除了根節點外,每一個子節點只存儲一個字符。go和golang共享go前綴,php、perl和python只共用p前綴。爲了實現字典排序,每一層節點上存儲的字符都是按照字典排序的方式存儲(這跟遍歷的方式有關)。咱們先來看看對單個字符如何進行字典排序。本文只考慮小寫字母,其它方式相似。'a'在'b'的前面,而'a'的ASCII碼小於'b'的ASCII碼,所以經過它們的ASCII相減就能夠獲得字典順序。並且python內置了字典排序的API,好比:golang

#!/usr/bin/env python
#coding: utf8

if __name__ == '__main__':
	arr = [c for c in 'python']
	arr.sort()
	print arr

$ python trie.py
['h', 'n', 'o', 'p', 't', 'y']

並且也可使用我以前的一篇文章介紹的bitmap來實現:Python: 實現bitmap數據結構 。實現代碼以下:算法

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up爲True則爲向上取整, 不然爲向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

	def calcBitIndex(self, num):
		return num % 31

	def set(self, num):
		elemIndex = self.calcElemIndex(num)
		byteIndex = self.calcBitIndex(num)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem | (1 << byteIndex)

	def clean(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem & (~(1 << byteIndex))

	def test(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		if self.array[elemIndex] & (1 << byteIndex):
			return True
		return False

if __name__ == '__main__':
	MAX = ord('z')
	suffle_array = [c for c in 'python']
	result       = []
	bitmap = Bitmap(MAX)
	for c in suffle_array:
		bitmap.set(ord(c))
	
	for i in range(MAX + 1):
		if bitmap.test(i):
			result.append(chr(i))

	print '原始數組爲:    %s' % suffle_array
	print '排序後的數組爲: %s' % result

$ python trie.py
原始數組爲:    ['p', 'y', 't', 'h', 'o', 'n']
排序後的數組爲: ['h', 'n', 'o', 'p', 't', 'y']

bitmap的排序不能有重複字符。其實剛纔所說的基於ASCII碼相減的方式進行字典排序,已經有不少成熟算法了,好比插入排序、希爾排序、冒泡排序和堆排序等等。本文爲了圖簡單,將使用Python自帶的sorted方法來進行單字符的字典排序。若是讀者自行實現單字符數組的排序也能夠,並且這樣將能夠自定義字符串的排序方式。shell

實現思路

整個實現包括2個類:Trie類和Node類。Node類表示Trie樹中的節點,由Trie類組織成一棵Trie樹。咱們先來看Node類:數組

#!/usr/bin/env python
#coding: utf8

class Node(object):
	def __init__(self, c=None, word=None):
		self.c          = c    # 節點存儲的單個字符
		self.word       = word # 節點存儲的詞
		self.childs     = []   # 此節點的子節點

Node包含三個成員變量。c爲每一個節點上存儲的字符。word表示一個完整的詞,在本文中指的是一個字符串。childs包含這個節點的全部子節點。既然在每一個節點中存儲了c,那麼存儲word有什麼用呢?而且這個word應該存在哪一個節點上呢?仍是用剛纔的圖舉例子:好比go和golang,它們共用go前綴,若是是字符串搜索倒好辦,由於會提供原始字符串,只要在這棵Trie樹上按照路徑搜索便可。可是對於排序來講,不會提供任何輸入,因此沒法知道單詞的邊界在哪裏,而Node類中的word就是起到單詞邊界做用。具體是存儲在單詞的最後一個節點上,如圖所示:微信

而Node類中的c成員若是這棵樹不用於搜索,則能夠不定義它,由於在排序中它不是必須的。數據結構

接下來咱們看看Trie類的定義:app

#!/usr/bin/env python
#coding: utf8

'''Trie樹實現字符串數組字典排序'''

class Trie(object):
	def __init__(self):
		self.root  = Node() # Trie樹root節點引用

	def add(self, word):
		'''添加字符串'''
		node = self.root
		for c in word:
			pos = self.find(node, c)
			if pos < 0:
				node.childs.append(Node(c))
				#爲了圖簡單,這裏直接使用Python內置的sorted來排序
				#pos有問題,由於sort以後的pos會變掉,因此須要再次find來獲取真實的pos
				#自定義單字符數組的排序方式能夠實現任意規則的字符串數組的排序
				node.childs = sorted(node.childs, key=lambda child: child.c)
				pos = self.find(node, c)
			node = node.childs[pos]
		node.word = word

	def preOrder(self, node):
		'''先序輸出'''
		results = []
		if node.word:
			results.append(node.word)
		for child in node.childs:
			results.extend(self.preOrder(child))
		return results

	def find(self, node, c):
		'''查找字符插入的位置'''
		childs = node.childs
		_len   = len(childs)
		if _len == 0:
			return -1
		for i in range(_len):
			if childs[i].c == c:
				return i
		return -1

	def setWords(self, words):
		for word in words:
			self.add(word)

Trie包含1個成員變量和4個方法。root用於引用根結點,它不存儲具體的數據,可是它擁有子節點。setWords方法用於初始化,調用add方法來初始化Trie樹,這種調用是基於每一個字符串的。add方法將每一個字符添加到子節點,若是存在則共用它並尋找下一個子節點,依此類推。find是用於查找是否已經創建了存儲某個字符的子節點,而preOrder是先序獲取存儲的word。樹的遍歷方式有三種:先序遍歷、中序遍歷和後序遍歷,若是各位不太明白,可自行Google去了解。接下咱們測試一下:

#!/usr/bin/env python
#coding: utf8

'''Trie樹實現字符串數組字典排序'''

class Trie(object):
	def __init__(self):
		self.root  = Node() # Trie樹root節點引用

	def add(self, word):
		'''添加字符串'''
		node = self.root
		for c in word:
			pos = self.find(node, c)
			if pos < 0:
				node.childs.append(Node(c))
				#爲了圖簡單,這裏直接使用Python內置的sorted來排序
				#pos有問題,由於sort以後的pos會變掉,因此須要再次find來獲取真實的pos
				#自定義單字符數組的排序方式能夠實現任意規則的字符串數組的排序
				node.childs = sorted(node.childs, key=lambda child: child.c)
				pos = self.find(node, c)
			node = node.childs[pos]
		node.word = word

	def preOrder(self, node):
		'''先序輸出'''
		results = []
		if node.word:
			results.append(node.word)
		for child in node.childs:
			results.extend(self.preOrder(child))
		return results

	def find(self, node, c):
		'''查找字符插入的位置'''
		childs = node.childs
		_len   = len(childs)
		if _len == 0:
			return -1
		for i in range(_len):
			if childs[i].c == c:
				return i
		return -1

	def setWords(self, words):
		for word in words:
			self.add(word)

class Node(object):
	def __init__(self, c=None, word=None):
		self.c          = c    # 節點存儲的單個字符
		self.word       = word # 節點存儲的詞
		self.childs     = []   # 此節點的子節點

if __name__ == '__main__':
	words = ['python', 'function', 'php', 'food', 'kiss', 'perl', 'goal', 'go', 'golang', 'easy']
	trie = Trie()
	trie.setWords(words)
	result = trie.preOrder(trie.root)
	print '原始字符串數組:     %s' % words
	print 'Trie樹排序後:       %s' % result
	words.sort()
	print 'Python的sort排序後: %s' % words

$ python trie.py
原始字符串數組:     ['python', 'function', 'php', 'food', 'kiss', 'perl', 'goal', 'go', 'golang', 'easy']
Trie樹排序後:       ['easy', 'food', 'function', 'go', 'goal', 'golang', 'kiss', 'perl', 'php', 'python']
Python的sort排序後: ['easy', 'food', 'function', 'go', 'goal', 'golang', 'kiss', 'perl', 'php', 'python']

結束語

樹的種類很是之多。在樹結構的實現中,樹的遍歷是個難點,須要多加練習。上述代碼寫得比較倉促,沒有進行任何優化,但在此基礎上能夠實現任何方式的字符串排序,以及字符串搜索等。

相關文章
相關標籤/搜索