从前文可以看出,ICTCLAS中DynamicArray类在初步分词过程中起到了至关重要的所用,而ICTCLAS中DynamicArray类的实现比较复杂,可以说是包罗万象,在一个GetElement方法就综合考虑了1)row优先排序的链表;2)col优先排序的链表;3)当nRow为-1时的行为;4)当nCol为-1时的行为;5)当nRow与nCol都不为-1时的行为 (可以参考本人的《天书般的ICTCLAS分词系统代码(一)》一文)。
为了简化编程接口,并将纠缠不清的代码逻辑剥离开来,我重新设计了DynamicArray类,利用三个类实现原有一个类的功能。具体改造包括:1) 将DynamicArray类做成一个抽象父类,实现一些公共功能;2)设计了RowFirstDynamicArray类与ColumnFirstDynaimcArray类作为DynamicArray的子类,分别实现row优先排序和col优先排序的DynamicArray。2) 在牺牲有限性能的同时力争大幅度简化代码的复杂度,提高可读性。
具体实现如下:
1、DynamicArray链表结点的定义
为了使得DynamicArray更具有通用性,使用了范型方式定义了链表的结点,代码如下:
{
public int row;
public int col;
public T Content;
public ChainItem<T> next;
}
2、DynamicArray类
DynamicArray类是一个抽象类,主要为RowFirstDynamicArray类与ColumnFirstDynaimcArray类提供公共的基础功能,例如查找行、列值为nRow, nCol的结点等。同时,该类将插入一新结点的方法设计成抽象方法,需要具体类根据row优先排序还是col优先排序自行决定具体实现。DynamicArray类的代码实现如下(应当说非常简单):
{
protected ChainItem<T> pHead; //The head pointer of array chain
public int ColumnCount, RowCount; //The row and col of the array
public DynamicArray()
{
pHead = null;
RowCount = 0;
ColumnCount = 0;
}
public int ItemCount
{
get
{
ChainItem<T> pCur = pHead;
int nCount = 0;
while (pCur != null)
{
nCount++;
pCur = pCur.next;
}
return nCount;
}
}
//====================================================================
// 查找行、列值为nRow, nCol的结点
//====================================================================
public ChainItem<T> GetElement(int nRow, int nCol)
{
ChainItem<T> pCur = pHead;
while (pCur != null && !(pCur.col == nCol && pCur.row == nRow))
pCur = pCur.next;
return pCur;
}
//====================================================================
// 设置或插入一个新的结点
//====================================================================
public abstract void SetElement(int nRow, int nCol, T content);
//====================================================================
// Return the head element of ArrayChain
//====================================================================
public ChainItem<T> GetHead()
{
return pHead;
}
//====================================================================
//Get the tail Element buffer and return the count of elements
//====================================================================
public int GetTail(out ChainItem<T> pTailRet)
{
ChainItem<T> pCur = pHead, pPrev = null;
int nCount = 0;
while (pCur != null)
{
nCount++;
pPrev = pCur;
pCur = pCur.next;
}
pTailRet = pPrev;
return nCount;
}
//====================================================================
// Set Empty
//====================================================================
public void SetEmpty()
{
pHead = null;
ColumnCount = 0;
RowCount = 0;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
ChainItem<T> pCur = pHead;
while (pCur != null)
{
sb.Append(string.Format("row:{0,3}, col:{1,3}, ", pCur.row, pCur.col));
sb.Append(pCur.Content.ToString());
sb.Append("\r\n");
pCur = pCur.next;
}
return sb.ToString();
}
}
3、RowFirstDynamicArray类的实现
RowFirstDynamicArray类主要实现了row优先排序的DynamicArray,里面包含了两个方法:GetFirstElementOfRow(获取row为nRow的第一个元素)和SetElement方法。其中GetFirstElementOfRow有两个重载版本。
这等价于将原有ICTCLAS中GetElement方法拆分成了三个方法,如果算上重载版本的话共五个方法,它们分别是:1)获取行、列值为nRow, nCol的结点,此方法由DynamicArray类实现;2)对于Row优先排序的链表而言,单独提供了GetFirstElementOfRow方法。3)对于Column优先排序的链表而言,单独提供了GetFirstElementOfColumn方法。
RowFirstDynamicArray类的实现如下:
{
//====================================================================
// 查找行为 nRow 的第一个结点
//====================================================================
public ChainItem<T> GetFirstElementOfRow(int nRow)
{
ChainItem<T> pCur = pHead;
while (pCur != null && pCur.row != nRow)
pCur = pCur.next;
return pCur;
}
//====================================================================
// 从 startFrom 处向后查找行为 nRow 的第一个结点
//====================================================================
public ChainItem<T> GetFirstElementOfRow(int nRow, ChainItem<T> startFrom)
{
ChainItem<T> pCur = startFrom;
while (pCur != null && pCur.row != nRow)
pCur = pCur.next;
return pCur;
}
//====================================================================
// 设置或插入一个新的结点
//====================================================================
public override void SetElement(int nRow, int nCol, T content)
{
ChainItem<T> pCur = pHead, pPre = null, pNew; //The pointer of array chain
if (nRow > RowCount)//Set the array row
RowCount = nRow;
if (nCol > ColumnCount)//Set the array col
ColumnCount = nCol;
while (pCur != null && (pCur.row < nRow || (pCur.row == nRow && pCur.col < nCol)))
{
pPre = pCur;
pCur = pCur.next;
}
if (pCur != null && pCur.row == nRow && pCur.col == nCol)//Find the same position
pCur.Content = content;//Set the value
else
{
pNew = new ChainItem<T>();//malloc a new node
pNew.col = nCol;
pNew.row = nRow;
pNew.Content = content;
pNew.next = pCur;
if (pPre == null)//link pNew after the pPre
pHead = pNew;
else
pPre.next = pNew;
}
}
}
有关ColumnFirstDynamicArray类的实现大同小异,这里就不再提供代码了。我们此时可以对比一下原有ICTCLAS中GetElement的实现:
PARRAY_CHAIN *pRet)
{
PARRAY_CHAIN pCur = pStart;
if (pStart == 0)
pCur = m_pHead;
if (pRet != 0)
*pRet = NULL;
if (nRow > (int)m_nRow || nCol > (int)m_nCol)
//Judge if the row and col is overflow
return INFINITE_VALUE;
if (m_bRowFirst)
{
while (pCur != NULL && (nRow != - 1 && (int)pCur->row < nRow || (nCol !=
- 1 && (int)pCur->row == nRow && (int)pCur->col < nCol)))
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
}
else
{
while (pCur != NULL && (nCol != - 1 && (int)pCur->col < nCol || ((int)pCur
->col == nCol && nRow != - 1 && (int)pCur->row < nRow)))
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
}
if (pCur != NULL && ((int)pCur->row == nRow || nRow == - 1) && ((int)pCur
->col == nCol || nCol == - 1))
//Find the same position
{
//Find it and return the value
if (pRet != 0)
*pRet = pCur;
return pCur->value;
}
return INFINITE_VALUE;
}
可以看出,将原有GetElement方法拆分成3个方法后,代码得到大大简化,而且逻辑更为清晰了。
3、性能与代码可读性的权衡
DynamicArray类为了确保代码的清晰可读,在某些地方做了些调整,让我们对比一下SharpICTCLAS与ICTCLAS中在这方面的不同考虑。下面的代码演示了GetFirstElementOfRow方法在两者之间的不同之处(我特意对ICTCLAS代码做了逻辑上的简化):
// SharpICTCLAS 中的查找行为 nRow 的第一个结点
//====================================================================
public ChainItem<T> GetFirstElementOfRow(int nRow)
{
ChainItem<T> pCur = pHead;
while (pCur != null && pCur.row != nRow)
pCur = pCur.next;
return pCur;
}
//====================================================================
// ICTCLAS 中的查找行为 nRow 的第一个结点
//====================================================================
... GetElement(int nRow, int nCol, PARRAY_CHAIN pStart, PARRAY_CHAIN *pRet)
{
PARRAY_CHAIN pCur = pStart;
while (pCur != NULL && (pCur->row < nRow || (pCur->row == nRow && pCur->col < nCol)))
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
if (pCur != NULL && pCur->row == nRow && pCur->col == nCol)
{
if (pRet != 0)
*pRet = pCur;
return pCur->value;
}
//......
}
从上面代码中可以看出,原有ICTCLAS代码充分考虑到DynamicArray是一个排序链表,因此仅仅在pCur->row < nRow与pCur->col < nCol范围内检索,如果找到了“pCur->row == nRow && pCur->col == nCol”,那么再去做该做的事情。
而SharpICTCLAS中,判断条件仅为“pCur != null && pCur.row != nRow”,这意味着如果你要找的nRow不再该链表中,则会来个“完全遍历”,搜索范围似乎太大了。
不过出于以下几点考虑我还是采用了这种表示方式:
1)汉语中的一句话不会太长,这意味着链表长度不会很长,即使来个“完全遍历”也不会牺牲多少时间。
2)毕竟要找的nRow不在该链表中的可能性不大,出现“完全遍历”的机会也不多。
3)原有ICTCLAS虽然在搜索范围内下了翻功夫,但为了确保pRet变量得到赋值,循环体内部多次执行了“if (pRet != 0)”的判断,这从性能角度上说得不偿失。
4)原有ICTCLAS为了缩小搜索范围确增加了条件判断次数“pCur != NULL && (pCur->row < nRow || (pCur->row == nRow && pCur->col < nCol))”,而由此带来的性能损失不得不考虑一下。
正因为以上几点考虑,所以在SharpICTCLAS中采用了这种简单而且并不见得低效的方式取代原有的GetElement方法。
- 小结
SharpICTCLAS重新设计了DynamicArray类,力争简化原有设计中复杂的代码逻辑,应当说效果比较明显。即便有性能损失,那也是微不足道的,权衡利弊,我选择了走简单的代码这条路。