问题大致描述:对英文文章进行分割,其中分割符为‘,’,和‘.' 。分割子句时要记住子句的起初位置和子句长度,并且,将长度相同的子句记录在一起。大概题意只记得这么多了。
面试官给的是双链表法,我当时给的不是这个方法的,后来又写了封信给他了的。以下为信的内容:
尊敬的面试官:
您好,很荣幸能够被您二面,对于您给出的句子分割问题的双链表解法,我个人持有不同观点。如下是我个人的分析,有不妥之处,还请批评指正。
方法一:双链表法
数据结构:
struct LNode
{
unsigned int index;
LNode* next;
};
struct Adjust
{
unsigned int len;
LNode* pNode;
Adjust* next;
};
图1 双链表示意图
设头链表长度为headLength,第i个头结点中pNode所指的链表长度为nodeLenth[i]。
插入操作:
对于长度为length、起始点为index的数据插入链表中,则先遍历头结点链表:
Adjust* pAdjust = head;
while (pAdjust != NULL)
{
if (pAdjust->len == length)
{//找到
LNode* pNode = new LNode; //新建结点
pNode->index= index;
pNode->next= pAdjust->pNode;//直接插入其后
pAdjust->pNode= pNode;
break;//找到则退出
}
}
if (pAdjust == NULL)
{//没找到则重新重建头结点并插入头链表中
pAdjust = new Adjust;//创建一个头结点插入头结点链表中,位于head之后
pAdjust->len= length;
pAdjust->next = head;
head = pAdjust;
LNode* pNode = new LNode;
pNode->index= index;
pNode->next= NULL;
pAdjust->pNode= pNode;
}
复杂度分析:
对于含有headLength个头结点的链表,遍历的最坏时间复杂度为O(headLength),平均值为(headLength+1)/2;
头结点占用的空间为: headLength*sizeof(Adjust);
查找操作:
在双链表中统计长度为length的句子个数,并且输出每个句子
void queary_length(unsignedint length)
{
assert(head!= NULL && buff!= NULL);
Adjust* pAdjust= head;
while (pAdjust != NULL)
{//时间复杂度O(headLength)
if (pAdjust->len == length)
{//找到
LNode* pNode = pAdjust->pNode;
unsigned int count = 0;//统计个数
while (pNode != NULL)
{//时间复杂度O(nodeLength[length])
count++;
for (int i=pNode->index; i<pNode->index+length; ++i)
{//复杂度O(length)
cout << buff[i];
}
}
cout<< "长度为"<< length << "的句子总数为:" << count << endl;
}
}//end while
} //end func
复杂度分析:
对于含有headLength个头结点的链表,统计长度为length的句子的个数并输出它们,
最坏情况下,时间复杂度为:O(headLength*nodeLenth[length]*length)。
双链表方法适用情况:
由上面的分析可知,当句子比较少,句子长度的种类N很小时,这种方法很优,headLength==N,头节点链表空间N*sizeof(Adjust);都是有效空间,并且不受句子长度的约束。
但是,当句子很多,N很大时,这种方法就不适用了。因为此时N*sizeof(Adjust)将会比N*sizeof(LNode*)大很多,在32位系统中,前者是后者的三倍。详细见方法二
方法二:hash+单链表法
数据结构:
#define MAXLENGTH 600 //最大句子长度
LNode*hash[MAXLENGTH];//hash[i]表示存放长度为i的句子信息的链表的表头
struct LNode
{
unsigned int index;
LNode* next;
};
图2 单链表结构示意图
int i = 0;
//数组初始化
for(i=0; i<MAXLENGTH; ++i)
{
hash[i] = NULL;
}
插入操作:
将长度为length、起始点为index 的数据插入单链表中。
//插入长度为length,起始坐标为index
if (length >= MAXLENGTH)
{
//重新开辟空间
}
else
{
LNode* pNode = new LNode;//建立新节点并插入
pNode->index= index;
pNode->next= hash[length];
hash[length] = pNode;
}
复杂度分析:
当length < MAXLENGTH 时,时间复杂度为O(1),当length>= MAXLENGTH时间复杂度为O(MAXLENGTH);
头结点空间复杂度为O(MAXLENGTH*sizeof(LNode*))。
查找操作:
void queary_length(char*buff, unsignedint length)
{
assert(buff != NULL length < MAXLENGTH);
LNode* pNode = hash[length];
unsigned int count = 0; //统计个数
while (pNode != NULL)
{//时间复杂度nodeLength[length]
count++;
for (int i=pNode->index; i<pNode->index+length; ++i)
{//时间复杂度O(length)
cout << buff[i];
}
}
cout << "长度为"<< length << "的句子总数为:" << count << endl;
} //end func
复杂度分析:
时间复杂度为O(nodeLenth[length]*length)。
Hash+单链表法的适用情况:
由于要提前开辟了MAXLENGTH个sizeof(LNode*)空间,可见,当句子很少,即句子长度种类N很小时,hash数组中很多头结点为空,即没有被利用上。
但是,当句子很多很多时,此时句子长度的种类N也会很大,此时hash数组中的每个节点都会被利用。只有当length >= MAXLENGTH,才会重新分配空间的。
注意:基于统计学,句子长度length >= MAXLENGTH (MAXLENGTH==600)的语句出现的概率有多大呢?如果,真出现了这样的语句,就重新给hash分配更大的空间,那么在这种情况下,长度在length左右的句子很有可能也会出现。故数组hash的空间不会被浪费。
两种方法的比较:
由上述可见,当句子长度种类数N很小时,双链表法很适用,而hash+单链表法就额外占用无效空间了,尽管此时查找插入和查找速度都很快。
当处理大量句子,句子长度种类数N很大时(接近但仍然小于MAXLENGHT),双链表法的头结点占用空间为N*sizeof(Adjust),而hash+单链表法占用空间为MAXLENGHT*sizeof(LNode*),
此时,前者占用空间约为后者的三倍。也就是说,当N > MAXLENGHT/3时,即N >200时,在空间上,用第二种方法就很划算了。插入时,双链表法的时间复杂度为O(N),而hash+单链表法的时间复杂度为O(1)。查询时,前者的时间复杂度为O(N*nodeLenth[length]*length),而后者的时间复杂度为O(nodeLenth[length]*length),最坏情况下,后者比前者快N倍。
综上可见,此题用hash+单链表法更好。STL中,hashtable用的就是这种结构。
方法三:
multimap<unsigned int, unsigned int>,以length为key,以index为value。这样岂不是更简单,只可惜回来路上才想到。
声明:此信内容仅仅作为我对此问题的二面答案的补充与分析,仅仅是为了技术上的交流,没有其他任何意思,也算是给自己一个交代吧。我一个因为这个被刷就够了,要是别人再因为这个被刷了,那就不好了。
General Manketon