Trie树:字符串频率统计排序

题目:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前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);
    原文作者:Trie树
    原文地址: https://blog.csdn.net/weixin_33895475/article/details/87241978
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞