题目:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前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
但是然后道题首先要同时没颗树出现的次数
最终题目的时间复杂度为:
总复杂度: O(nle) + O(nlg10);
作者:张晓天a
链接:https://www.jianshu.com/p/c6a96c1fcbb7