日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

天天学算法——搜索热词关联(TopK)

發(fā)布時(shí)間:2023/12/20 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 天天学算法——搜索热词关联(TopK) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

目錄:

  • 《劍指offer》面試題-topk算法
  • 搜索熱詞關(guān)聯(lián)算法
  • 代碼實(shí)現(xiàn)以及java學(xué)習(xí)

寫(xiě)在前面

每次寫(xiě)博客都愛(ài)先扯點(diǎn)亂七八糟的東西,這是工作準(zhǔn)備寫(xiě)的第2篇博客,之前寫(xiě)過(guò)一篇hadoop入門(mén),那里還留下了一個(gè)搜索引擎的demo沒(méi)有去完成,這次學(xué)習(xí)熱詞關(guān)聯(lián)剛好也是和搜索引擎相關(guān),所以借此機(jī)會(huì)把這篇記錄下來(lái),一方面花了3天來(lái)學(xué)習(xí)了這個(gè)內(nèi)容,確實(shí)學(xué)到了不少東西,二來(lái)下次寫(xiě)搜索引擎的hadoop的demo時(shí)候可以把這個(gè)整合到一起,如果有空把關(guān)于搜索的東西整合到一起,添加一些爬蟲(chóng)相關(guān)的只是內(nèi)容,就可以簡(jiǎn)單的搭建一個(gè)搜索引擎了,想想還是挺不錯(cuò)的。好啦,我們來(lái)開(kāi)始學(xué)習(xí)吧!

topK算法


這個(gè)題目實(shí)現(xiàn)不難,在沒(méi)有什么限制的情況下我們很快能得到答案。

解法1 排序

對(duì)數(shù)組排序,然后找到最小的k個(gè)數(shù)字,這個(gè)思路和粗暴,實(shí)際上我們把問(wèn)題轉(zhuǎn)化成了排序算法,那么合理的想法就是去找排序中時(shí)間復(fù)雜度最小的快排(O(nlgn)),這里對(duì)于此方法有一個(gè)問(wèn)題就是在于需要改變?cè)瓟?shù)組,如果題目中存在此種限制,自然是需要考慮其他算法。

解法2 partition算法

parition算法,說(shuō)起這個(gè)算法可能對(duì)于算法不太熟悉的同學(xué)真沒(méi)什么印象,但是如果說(shuō)快排大家肯定都知道了。我先貼一段java實(shí)現(xiàn)的代碼給大家看一看。

//快速排序 雖然快排思想比較簡(jiǎn)單,但是有些=還是需要注意一下勒,網(wǎng)上不少博客的代碼都有點(diǎn)小問(wèn)題,所以自己寫(xiě)了跑了下才貼出來(lái)。public static void qsort(int[] arr,int begin,int end) {int index = partition(arr, begin,end);if(begin >= end -1) {return; }qsort(arr,begin,index-1);qsort(arr,index+1,end);} //用一個(gè)哨兵來(lái)分割大于小于的兩部分private static int partition(int[] arr,int begin,int end) {if(begin >= end) {return -1;}int pos = arr[begin];while(begin < end) {while(arr[end] >= pos && begin < end) {end --;}if(begin < end) {arr[begin] = arr[end];}while(arr[begin] <= pos && begin < end) {begin ++;}if(begin < end) {arr[end] = arr[begin];}}arr[begin] = pos;return begin;}

以上代碼中有很重要的一塊就是partition,很多快排的寫(xiě)法里面沒(méi)有將其作為單獨(dú)的一個(gè)函數(shù),他的思想就是取出一個(gè)哨兵,然后把大于他的放到一邊,小于他的放到另一邊。這樣如果我們按著這個(gè)思路,先找到一個(gè)數(shù)partition一次,判斷這個(gè)樹(shù)的最終位置是不是在k處,如果大于則找前面的部分(假設(shè)左小右大),如此直到我們找到第k個(gè)值的位置,此時(shí)k之前的都比k小,就得到了題解。下面我大概舉個(gè)例子,給大家一個(gè)形象的表示。
arr = 4,3,5,9,2,4,6 找到最小的3個(gè)值
partition1 2 3 4 9 5 4 6 index = 3 分了一次剛好index返回3,所以最小的是2 3 4,對(duì)沒(méi)毛病!
那我們現(xiàn)在來(lái)看一看這個(gè)算法的時(shí)間復(fù)雜度,逆序的時(shí)候復(fù)雜度最高為O(n^2),如果是隨機(jī)的話,T(N) = T(T/2) + N,時(shí)間復(fù)雜度為O(N)。那么我們可以在O(N)的時(shí)間復(fù)雜度把這個(gè)問(wèn)題給解決了。這比上述排序好多了,因?yàn)獒槍?duì)上述排序中,我們每次都要把序列找到一個(gè)哨兵然后左右都要去排序,這個(gè)時(shí)候,我們只處理我們需要處理的部分,時(shí)間復(fù)雜度就降低了下來(lái)。雖然簡(jiǎn)單,還是畫(huà)個(gè)圖表示一下下。如下圖,如果我們想要去找前3小的數(shù)字時(shí),如果哨兵是5,那么我們就可以不用管后面部分,只需要考慮前面綠色填充的數(shù)字,這樣節(jié)約了很多時(shí)間。

但是這個(gè)算法仍然有點(diǎn)問(wèn)題,同解法1,這個(gè)算法會(huì)調(diào)整數(shù)據(jù),當(dāng)數(shù)據(jù)量不斷增加時(shí),我們有時(shí)候希望能增量式的去處理,而不是每次有數(shù)據(jù)進(jìn)來(lái)都乾坤大挪移,那么我們需要考慮外部存儲(chǔ)來(lái)輔助這個(gè)算法保證這個(gè)原數(shù)組不會(huì)改變。

解法3 外部存儲(chǔ)-小(大)根堆

我們?nèi)粘R矔?huì)遇到這樣的算法例子,偶爾我們會(huì)用一個(gè)外部數(shù)組來(lái)存儲(chǔ),每次進(jìn)來(lái)一個(gè)數(shù)字就判斷。比如我們想找一個(gè)數(shù)組里面最大的3個(gè)數(shù)字,我開(kāi)一個(gè)3空間的數(shù)組,那么我們遍歷一次原數(shù)組,找到最大的3個(gè)依次放入,每次放入時(shí)和外部數(shù)組比較一下,這樣也可以在O(N)時(shí)間內(nèi)完成,且不改變?cè)瓟?shù)組,好啦。貌似我們對(duì)這個(gè)算法已經(jīng)了解的很深入了。
且慢,各位看客想一想,如果這個(gè)N非常非常大時(shí)候,如果我們要在幾百萬(wàn)的數(shù)據(jù)中找前10,那會(huì)有什么不同么。對(duì)于算法復(fù)雜度來(lái)說(shuō),O(N)應(yīng)該是不可避免了,至少每個(gè)數(shù)字都要遍歷到,但是對(duì)于大數(shù)據(jù)處理來(lái)說(shuō),復(fù)雜度中隱藏的時(shí)間常熟因子也是十分關(guān)鍵的。我們現(xiàn)在來(lái)分析一波,對(duì)于外部數(shù)組,如果我們是找最大的K個(gè)數(shù),那么我們每次需要找到數(shù)組中最小的,如果最小我們就要替換,所以會(huì)有替換操作。那么對(duì)于一個(gè)無(wú)順序數(shù)組的話,大概O(K)可以完成,然后我們算法整體就是O(K*N),如果我們來(lái)維護(hù)一個(gè)有序數(shù)組的話,開(kāi)銷(xiāo)沒(méi)什么區(qū)別。如果熟悉數(shù)據(jù)結(jié)構(gòu)的同學(xué),現(xiàn)在一定看出問(wèn)題了,我們需要用堆來(lái)完成這些操作,取最小堆可以O(shè)(1),來(lái)完成,而插入堆也可以在O(lgN)完成(平均),OK,數(shù)據(jù)量一大時(shí)候,這個(gè)差異是非常大的,先給大家看一個(gè)感性的認(rèn)識(shí),我沒(méi)有具體去算時(shí)間,只是進(jìn)行了一下對(duì)比,heap為我自己實(shí)現(xiàn)的小根堆,orderarr是網(wǎng)上借鑒的別人實(shí)現(xiàn)的有序數(shù)組。下面應(yīng)該十分明顯了,k小時(shí)沒(méi)有啥區(qū)別,k的變大,這個(gè)差距會(huì)越來(lái)越大。

int n = 3000000;int k = 100;orderarr程序運(yùn)行時(shí)間: 16msheap程序運(yùn)行時(shí)間: 13msint n = 3000000;int k = 100000;orderarr程序運(yùn)行時(shí)間: 5137msheap程序運(yùn)行時(shí)間: 59ms

算法不難,此處介紹一下思路即可,晚上有很多介紹堆思路的,算法導(dǎo)論中有heapify來(lái)維護(hù)堆的,java中的優(yōu)先隊(duì)列好像是用shiftup,shitfdown來(lái)維護(hù)insert操作,個(gè)人覺(jué)得都可以,思想都是一致的。大家有興趣可以翻翻我的github,文末給出,我把這些代碼都放在里面,有不對(duì)之處大家也可以指教。

public static void testHeap(int n,int k) {int[] arr = new int[n];Random random = new Random();for(int i=0;i<arr.length;i++) {arr[i] = random.nextInt();}int length = arr.length;MinHeap mHeap = new MinHeap();long startTime=System.currentTimeMillis(); //獲取開(kāi)始時(shí)間 for(int i=0;i<length;i++) {if(i<=k-1) {mHeap.insert(arr[i]);}else {if(arr[i] > mHeap.getTop()) {mHeap.removeTop();mHeap.insert(arr[i]);}}} // mHeap.show();long endTime=System.currentTimeMillis(); //獲取結(jié)束時(shí)間 System.out.println("heap程序運(yùn)行時(shí)間: "+(endTime-startTime)+"ms"); }

熱詞搜索提示

現(xiàn)在終于到正題了,之前半天都是在介紹算法,現(xiàn)在也講講該算法的應(yīng)用,現(xiàn)在xx搜索引擎公司需要根據(jù)用戶的輸入來(lái)給其他用戶做輸入提示,那么我們有很多輸入詞條,現(xiàn)在需要提示熱度最高的。這實(shí)際就是一個(gè)topK問(wèn)題,額外之處一個(gè)是操作對(duì)象不在是一個(gè)整數(shù),而是一個(gè)鍵值對(duì),另外一個(gè)問(wèn)題是我們需要構(gòu)建一顆trie樹(shù)來(lái)幫助我們找到需要排序的詞語(yǔ)。當(dāng)然對(duì)于日志信息來(lái)說(shuō),數(shù)據(jù)是一條一條的,我們還需要用到hash表來(lái)幫我們進(jìn)行統(tǒng)計(jì)。

第一步 hashMap統(tǒng)計(jì)

對(duì)于hash表來(lái)說(shuō),沒(méi)有特別要多說(shuō)的,統(tǒng)計(jì)一個(gè)大數(shù)據(jù)量,如果內(nèi)存夠的話,一張hash表無(wú)疑是很好的選擇,O(n)的時(shí)間就可以搞定,當(dāng)然這個(gè)大數(shù)據(jù)也是有一個(gè)限制的,如果上T或者更大,那可能就需要想其他的辦法了。G級(jí)別的這個(gè)還是沒(méi)問(wèn)題的。此處我們使用java中的hashMap來(lái)完成。

第二步 構(gòu)建trie樹(shù)

因?yàn)樯婕暗綉?yīng)用,當(dāng)輸入“北”的時(shí)候,希望能提示“北京”,或者“北海”,不能提示“南京”吧,那么我們需要有一顆前綴樹(shù)來(lái)實(shí)現(xiàn),每次找到輸入的節(jié)點(diǎn)的子樹(shù),對(duì)子樹(shù)中的節(jié)點(diǎn)遍歷,取得最大的K個(gè),為了方便,前綴樹(shù)結(jié)構(gòu)如下,每個(gè)節(jié)點(diǎn)放置到當(dāng)前節(jié)點(diǎn)位置的所有字符,并且添加對(duì)應(yīng)頻次,路過(guò)的詞語(yǔ)頻次為0,結(jié)構(gòu)圖大致如下。

第三部 topK算法

topK說(shuō)的很多了,我們需要改成能針對(duì)鍵值對(duì)的就OK啦! ~^_^~

代碼實(shí)現(xiàn)以及java學(xué)習(xí)

決策樹(shù)部分

下面是決策樹(shù)的實(shí)現(xiàn),決策樹(shù)中學(xué)習(xí)到的一個(gè)比較重要的點(diǎn)就是需要自己實(shí)現(xiàn)一個(gè)迭代器,之前的數(shù)組可以直接遍歷,for循環(huán)就可以了,但是樹(shù)沒(méi)有這么簡(jiǎn)單,便需要實(shí)現(xiàn)一個(gè)iterator來(lái)幫助完成遍歷。

首先class需要實(shí)現(xiàn)Iterable接口,調(diào)用x.iterator()返回一個(gè)Iterator類(lèi),這個(gè)類(lèi)通常含有2個(gè)方法,hasnext(),next()。結(jié)構(gòu)如下,具體請(qǐng)查看其他介紹,此處不贅述。 public class MyIteratorClass implements Iterable{@Overridepublic Iterator iterator() {// TODO Auto-generated method stubreturn new MyIterator;}private class MyIterator implements Iterator<TrieNode>{@Override//返回是否還有下一個(gè)值public boolean hasNext() {return null;}@Override//返回下一個(gè)迭代的值public TrieNode next() {return null;} import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Queue; import datastructure.TrieTree.TrieNode;public class TrieTree implements Iterable{ private TrieNode root;public static void main(String[] args) {TrieTree trieTree = new TrieTree();trieTree.insert("北京動(dòng)物園", 2);trieTree.insert("北京天安門(mén)", 3);trieTree.insert("北京", 1);String word = "北京";TrieNode subTree = trieTree.getSubTreeByWord(word);Iterator<TrieNode> iterator = trieTree.iterator(subTree);while(iterator.hasNext()) {TrieNode node = iterator.next();System.out.println(node.value + " " + node.count);}//trieTree.showTrieTree();}public TrieNode getRoot() {return root;}public TrieTree() {root = new TrieNode("root",0);}public class TrieNode{private String value;private ArrayList<TrieNode> son;private int count; //當(dāng)前路徑上統(tǒng)計(jì)數(shù)public TrieNode() {// TODO Auto-generated constructor stubthis.value = "null";this.count = 0;this.son = new ArrayList<TrieNode>();}public TrieNode(String value,int count) {// TODO Auto-generated constructor stubthis.value = value;this.count = count;this.son = new ArrayList<TrieNode>();}public String getValue() {return value;}public int getCount() {return count;}}//根據(jù)輸入獲取子樹(shù)public TrieNode getSubTreeByWord(String str) {return _getSubTreeByWord(root,str);}private TrieNode _getSubTreeByWord(TrieNode root,String str) {int sonNum = root.son == null? 0 :root.son.size();if(root.value.equals(str)) {return root;}for(int i=0;i<sonNum;i++) {TrieNode node = _getSubTreeByWord(root.son.get(i),str);if(node != null) {return node;}}return null;}//插入時(shí),把count放在最后一個(gè)節(jié)點(diǎn)上public void insert(String str,int count) {_insertNode(root, str, count ,1);}private void _insertNode(TrieNode root,String str,int count ,int index) {int sonNum = root.son.size();int findFlag = 0;for(int i=0;i<sonNum;i++) {if(root.son.get(i).value.equals(str.substring(0, index))) {findFlag = 1;if(str.length() == index) {root.son.get(i).count = count;return;}else {_insertNode(root.son.get(i), str, count ,index+1);}break;}}//遍歷之后沒(méi)有找到就創(chuàng)建一個(gè)if(findFlag == 0) {// System.out.println(str.substring(0, index));String newValue = str.substring(0, index);int newCount = index != str.length() ? 0 : count;TrieNode sonNode = new TrieNode(newValue,newCount);root.son.add(sonNode);if(str.length() != index) {_insertNode(sonNode, str, count ,index+1);}}}//循環(huán)遍歷輸出字典樹(shù)內(nèi)容public void showTrieTree() {_showTrieTree(root);}private void _showTrieTree(TrieNode root) {System.out.println(root.value + root.count);int sonNum = root.son.size();for(int i=0;i<sonNum;i++) {_showTrieTree(root.son.get(i));}}@Overridepublic Iterator<TrieNode> iterator() {// TODO Auto-generated method stubreturn new TrieTreeIterator();}public Iterator<TrieNode> iterator(TrieNode itrRoot) {// TODO Auto-generated method stubreturn new TrieTreeIterator(itrRoot);}private class TrieTreeIterator implements Iterator<TrieNode>{private TrieNode next;private Queue<TrieNode> queue;public TrieTreeIterator() {// TODO Auto-generated constructor stubnext = root;queue = new LinkedList<TrieNode>();if(next == null) {return;}}public TrieTreeIterator(TrieNode itrRoot) {// TODO Auto-generated constructor stubnext = itrRoot;queue = new LinkedList<TrieNode>();if(next == null) {return;}}@Overridepublic boolean hasNext() {// TODO Auto-generated method stubint sonNum = next.son.size();for(int i=0;i<sonNum;i++) {queue.add(next.son.get(i));}if(queue.isEmpty()) {return false;}else {return true;}}@Overridepublic TrieNode next() {// TODO Auto-generated method stubnext = queue.remove();return next;}}}

Heap部分

此處借鑒了網(wǎng)友的代碼,但是不記得哪里抄來(lái)的了,抱歉。
這里學(xué)到的比較重要的東西就是泛型的使用,對(duì)于這份代碼來(lái)說(shuō),是適用與int的,但是我想拿這份代碼來(lái)做鍵值對(duì)的處理,利用泛型和提供的Comparator就可以很方便的實(shí)現(xiàn)代碼的復(fù)用。此處我定義了鍵值對(duì)的類(lèi)型,提供了一些基礎(chǔ)方法。然后出現(xiàn)了另外一個(gè)重要的問(wèn)題,那就是關(guān)于對(duì)象復(fù)制的問(wèn)題,這里學(xué)習(xí)了深克隆的方式,如果setRoot方法不傳入clone(),則只是傳入了索引,而不是對(duì)堆內(nèi)進(jìn)行賦值,這樣邏輯上有誤。所以這里傳入一定是clone的內(nèi)容。關(guān)于clone有深淺之分,這里我使用了序列化的方式,下面這博客寫(xiě)的不錯(cuò),推一推。
克隆學(xué)習(xí):https://www.cnblogs.com/Qian123/p/5710533.html

//代碼復(fù)用可以學(xué)習(xí)的地方//這是之前的代碼 int[] arr = new int[n] //此處省略,很多,這里重點(diǎn)在于描述差異之處 mHeap.setRoot(arr[i]);//這是修改后的代碼 KeyPair<String, Integer> tempKeyPair = new KeyPair<String, Integer>("", 0); mHeap.setRoot(tempKeyPair.clone()); //建立heap來(lái)做鍵值對(duì)比較 Comparator<KeyPair<String, Integer>> comp = new Comparator<KeyPair<String, Integer>>() { @Overridepublic int compare(KeyPair<String, Integer> o1, KeyPair<String, Integer> o2) {// TODO Auto-generated method stubreturn o2.getValue() - o1.getValue();} }; //克隆可以學(xué)習(xí)的部分,序列化方式克隆。public class KeyPair<K, V> implements Serializable {private K key;private V value;public KeyPair clone() {KeyPair outer = null;try { // 將該對(duì)象序列化成流,因?yàn)閷?xiě)在流里的是對(duì)象的一個(gè)拷貝,而原對(duì)象仍然存在于JVM里面。所以利用這個(gè)特性可以實(shí)現(xiàn)對(duì)象的深拷貝ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(baos);oos.writeObject(this);// 將流序列化成對(duì)象ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bais);outer = (KeyPair) ois.readObject();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}return outer;}} public class Heap<T> { /** * 以數(shù)組形式存儲(chǔ)堆元素 */ private T[] heap; /** * 用于比較堆中的元素。c.compare(根,葉子) > 0。 * 使用相反的Comparator可以創(chuàng)建最大堆、最小堆。 */ private Comparator<T> c; public Heap(T[] a, Comparator<T> c) { this.heap = a.clone(); this.c = c; buildHeap(); } /** * 返回值為i/2 * * @param i * @return */ private int parent(int i) { return (i - 1) >> 1; } /** * * 返回指定節(jié)點(diǎn)的left子節(jié)點(diǎn)數(shù)組索引。相當(dāng)于2*(i+1)-1 * * * @param i * @return */ private int left(int i) { return ((i + 1) << 1) - 1; } /** * 返回指定節(jié)點(diǎn)的right子節(jié)點(diǎn)數(shù)組索引。相當(dāng)于2*(i+1) * * @param i * @return */ private int right(int i) { return (i + 1) << 1; } /** * 堆化 * * @param i * 堆化的起始節(jié)點(diǎn) */ private void heapify(int i) { heapify(i, heap.length); } /** * 堆化, * * @param i * @param size 堆化的范圍 */ private void heapify(int i, int size) { int l = left(i); int r = right(i); int next = i; if (l < size && c.compare(heap[l], heap[i]) > 0) next = l; if (r < size && c.compare(heap[r], heap[next]) > 0) next = r; if (i == next) return; swap(i, next); heapify(next, size); } /** * 對(duì)堆進(jìn)行排序 */ public void sort() { // buildHeap(); for (int i = heap.length - 1; i > 0; i--) { swap(0, i); heapify(0, i); } } /** * 交換數(shù)組值 * * @param arr * @param i * @param j */ private void swap(int i, int j) { T tmp = heap[i]; heap[i] = heap[j]; heap[j] = tmp; } /** * 創(chuàng)建堆 */ private void buildHeap() { for (int i = (heap.length) / 2 - 1; i >= 0; i--) { heapify(i); } } public void setRoot(T root) { heap[0] = root; heapify(0); } public T root() { return heap[0]; } public T getByIndex(int i) {if(i<heap.length) {return heap[i];}return null;} }

總結(jié)

上面貼出了測(cè)試數(shù)據(jù)和最終結(jié)果,以上代碼估計(jì)跑不通,貼出來(lái)是為了突出重點(diǎn),如果想看跑,可以follow我的github,上面有完整實(shí)例,另外還有一些問(wèn)題沒(méi)有解決完全,一個(gè)是大數(shù)據(jù)下的測(cè)試,另外有一個(gè)就是交互問(wèn)題,因?yàn)榭紤]到后面還會(huì)寫(xiě)相關(guān)搜索引擎的板塊,所以這里先不急著完善,以后有機(jī)會(huì)再做交互和完整測(cè)試。

哈,寫(xiě)了3個(gè)多小時(shí),終于總結(jié)完了~,看完有收貨的小伙伴,記得贊一個(gè)噢~。我們一起天天學(xué)算法~

github:https://github.com/Continue7777/algorithms

總結(jié)

以上是生活随笔為你收集整理的天天学算法——搜索热词关联(TopK)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。