题目:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
首先我们给出答案:
1. 建立Trie树,记录每颗树的出现次数,O(n*le); le:平均查找长度
2. 维护一个10的小顶堆,O(n*lg10);
3. 总复杂度: O(n*le) + O(n*lg10);
接着我们再分析:
根据题目的意思,我们知道就是对每一个单词进行计数,计数完成后进行排序。
如果学过数据结构的一定会想起hash,我们可以使用hashMap进行实现,但是key是一个字符串,大概率会出现冲突。
而冲突的解决就需要消耗时间。
我们先来计算hashmap的时间复杂度
Hash 表号称是 O(1) 的,但在计算 hash 的时候就肯定会是 O(k) ,而且还有碰撞之类的问题;
所以其时间复杂度大于O(k).
直接定址法-hash算法
我们都知道hash算法查找时间复杂度是O(1),但是如果遇到了冲突就会退化成线性查找。
int * n = new int[ 50000 ] ;
for ( int i = 0 ; i < 50000 ; ++i )
{
n[ i ] = rand( ) % 100 ;
}
// 统计每个数字出现个次数
map< int , int > Counter ;
for ( int i = 0 ; i < 50000 ; ++i )
{
++Counter[ n[ i ] ] ;
}
但是冲突解决很费时间,因为本身就是数字我们可以使用直接定址法,就是根据字符本身的号进行定位,这样就一定不会产生冲突。
统计每个数字出现个次数
int Counter[ 100 ] = { 0 } ;
for ( int i = 0 ; i < 50000 ; ++i )
{
++Counter[ n[ i ] ] ;
}
这就是直接定址法,是hash的一种特殊的形式,效率很高,不会存在冲突。
但是当key从数字变为字符串,如何确定字符串的唯一位置。
Trie树
要唯一的确定字符串的位置,我们首先想到的就是字典,对单词进行字典排序后,每一个单词的位置就是确定的了。
那么如何优化对“字典”的插入和查询,我们想到了树。
Trie 的强大之处就在于它的时间复杂度。它的插入和查询时间复杂度都为 O(k) 。
而且其中的K为单词的长度。同时其不会产生任何碰撞,所以其最大的时间复杂度为O(k)
但是当字符串的重复率较大,数据较多时,这个时间复杂差的还是比较大的。
简单地说,Trie就是直接定址表和树的结合的产物。
但是Trie树占据的空间还是比较大的。
class TrieNode // 字典树节点
{
private int num;// 有多少单词通过这个节点,即由根至该节点组成的字符串模式出现的次数
private TrieNode[] son;// 所有的儿子节点
private boolean isEnd;// 是不是最后一个节点
private char val;// 节点的值
//对每一个节点的初始化
TrieNode()
{
num = 1;
son = new TrieNode[SIZE];
isEnd = false;
}
}
堆排序
但我们计算每一个单词的重复数量后,就涉及到一个统计排序的问题,我们的目的是取出其中的前10个。
排序算法大家都已经不陌生了,,我们要注意的是排序算法的时间复杂度是NlgN。
题目要求是求出Top 10,因此我们没有必要对所有的数据都进行排序,我们只需要维护一个10个大小的数组,每读一条记录就和数组最后一个数据对比,如果小于这个数据,那么继续遍历,否则,将数组中的数据进行调整。
算法的最坏时间复杂度是N*K, 其中K是指top多少。
但是每次调整前K数据数据的时间复杂度是K,因为我们采用的是顺序比较,可是前K数组是有序的可以进行二分查找,可以将查找的时间复杂度变为logk,但是确定插入数据的位置,而数据的移动又变为一大问题。
有没有一种既能快速查找,又能快速移动元素的数据结构呢?
回答是肯定的,那就是堆。
借助堆结构,我们可以在log量级的时间内查找和调整/移动。
public class HeapSort {
private static void heapSort(int[] arr) {
int len = arr.length -1;
for(int i = len/2 - 1; i >=0; i --){ //堆构造
heapAdjust(arr,i,len);
}
while (len >=0){
swap(arr,0,len--); //将堆顶元素与尾节点交换后,长度减1,尾元素最大
heapAdjust(arr,0,len); //再次对堆进行调整
}
}
public static void heapAdjust(int[] arr,int i,int len){
int left,right,j ;
while((left = 2*i+1) <= len){ //判断当前父节点有无左节点(即有无孩子节点,left为左节点)
right = left + 1; //右节点
j = left; //j"指针指向左节点"
if(j < len && arr[left] < arr[right]) //右节点大于左节点
j ++; //当前把"指针"指向右节点
if(arr[i] < arr[j]) //将父节点与孩子节点交换(如果上面if为真,则arr[j]为右节点,如果为假arr[j]则为左节点)
swap(arr,i,j);
else //说明比孩子节点都大,直接跳出循环语句
break;
i = j;
}
}
public static void swap(int[] arr,int i,int len){
int temp = arr[i];
arr[i] = arr[len];
arr[len] = temp;
}
public static void main(String[] args) {
int array[] = {20,50,20,40,70,10,80,30,60};
System.out.println("排序之前:");
for(int element : array){
System.out.print(element+" ");
}
heapSort(array);
System.out.println("\n排序之后:");
for(int element : array){
System.out.print(element+" ");
}
}
}
最终的时间复杂度就降到了N*logK
但是然后道题首先要同时没颗树出现的次数
最终题目的时间复杂度为: