第六課主要介紹圖,不常常考,故今天先講第七課的內容,介紹比較常考的樹和貪心算法node
何爲前綴樹? 如何生成前綴樹?面試
能夠查有多少個字符串以「be」爲前綴。算法
若是要判斷有沒有「be」這個節點,每一個節點上加上一個數據項,有多少個字符串以當前節點結尾的(能夠查加了多少次特定字符串)。api
給一個字符串、返回多少個字符串以這個爲前綴。數組
再加一個數據項,記錄該節點被劃過多少次。less
大概實現:dom
刪除邏輯:ide
根據path是否變爲0,來判斷是否繼續往下刪。ui
能夠解決如下問題:this
一個字符串類型的數組arr1,另外一個字符串類型的數組arr2。
arr2中有哪些字符,是arr1中出現的?請打印
arr2中有哪些字符,是做爲arr1中某個字符串前綴出現的?請打印
arr2中有哪些字符,是做爲arr1中某個字符串前綴出現的?請打印arr2中出現次數最大的前綴。
public class Code_01_TrieTree { public static class TrieNode { public int path;//多少個到達過節點 public int end;//多少個以這個節點結尾 public TrieNode[] nexts;//表示下一個節點的狀況 public TrieNode() { path = 0; end = 0; //26個字母 nexts = new TrieNode[26]; } } public static class Trie { private TrieNode root; public Trie() { root = new TrieNode(); } public void insert(String word) { if (word == null) { return; } char[] chs = word.toCharArray(); TrieNode node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = chs[i] - 'a';//a index 0 -- b index 1 -- ...字母對應的 if (node.nexts[index] == null) {//沒有路就新建出來 node.nexts[index] = new TrieNode(); } node = node.nexts[index]; node.path++; } node.end++; } public void delete(String word) { if (search(word) != 0) { char[] chs = word.toCharArray(); TrieNode node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = chs[i] - 'a'; if (--node.nexts[index].path == 0) { node.nexts[index] = null; return; } node = node.nexts[index]; } node.end--; } } public int search(String word) { if (word == null) { return 0; } char[] chs = word.toCharArray(); TrieNode node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = chs[i] - 'a'; if (node.nexts[index] == null) { return 0; } node = node.nexts[index]; } return node.end; } public int prefixNumber(String pre) { if (pre == null) { return 0; } char[] chs = pre.toCharArray(); TrieNode node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = chs[i] - 'a'; if (node.nexts[index] == null) { return 0; } node = node.nexts[index]; } return node.path; } } public static void main(String[] args) { Trie trie = new Trie(); System.out.println(trie.search("zuo")); trie.insert("zuo"); System.out.println(trie.search("zuo")); trie.delete("zuo"); System.out.println(trie.search("zuo")); trie.insert("zuo"); trie.insert("zuo"); trie.delete("zuo"); System.out.println(trie.search("zuo")); trie.delete("zuo"); System.out.println(trie.search("zuo")); trie.insert("zuoa"); trie.insert("zuoac"); trie.insert("zuoab"); trie.insert("zuoad"); trie.delete("zuoa"); System.out.println(trie.search("zuoa")); System.out.println(trie.prefixNumber("zuo")); } }
貪心策略正確性的證實,千萬不要糾結,由於糾結你能死去。
想練貪心,記貪心的點就夠了,寫對數器的方式來驗證。
不用去糾結哪一個策略,證實上哪一個是對的。這是一個大套路。
字典序介紹
例如abc和bce對比,想象成26進制的數進行對比。
長度不等的時候,把短的後面擴長來對比,後補最小的單位。
題目:給定一個字符串類型的數組strs,找到一種拼接方式,使得把全部字符串拼起來以後造成的字符串具備最低的字典序。
什麼叫作貪心?
定義一個指標,在這個指標下,把每個樣本分出一個優先來,按照優先大的先執行,優先小的後執行,這就是貪心。
貪心就是一件事情,貪心就是某一種簡潔的標準,在這個標準下把全部東西分出個一二三來,而後根據這個優先級,決定一種順序,就是貪心。
貪心案例:
貪心就是找距離目標最近的拿,無論最終的結果,只注重局部或者眼前的結果的方法
按照這種方法就是有可能喪失全局最優的
就比如兩我的分5個大餅,一次能夠拿一個或者兩個,不吃完不容許再拿
A:用貪心方法:第一次拿兩個,B拿一個,那麼B最終拿3個
這個就是貪心方法失敗的一個例子
有的時候貪心算法也是能夠獲得最優解的,好比仍是大餅的例子,可是如今總數是7個了
A繼續貪心,先兩個,再兩個就是4個了;
而B先1個,後2個,就只能是3個
介紹完貪心就回到題目:
若是對數組進行排序,再拼接一塊兒,那是不行的。
正確的排序策略:
str1作前綴str2跟後面。和相反着對比。(每一個字符串做爲前綴貼在一塊兒比較)
排序策略不成立的例子。
比較策略沒有傳遞性,是一個環。
有傳遞性是:
這個題比較策略就是貪心策略。要肯定的一件事情,排序策略是有傳遞性的。
把a.b看作成K進制的數,「abc」 「efg」 abcefg,其實就是abc向左移動了b這個數的長度位,再加入efg。
等式兩邊減b乘c,第二個式子減b乘a
而後再化簡。
展開後ac抹掉,同時除與b,a移過去右,c移過去左...
證實排序策略是有傳遞性,是一個對的排序:
還要證實用這種排序策略獲得的序列,就是字典序最小的。
先證實一件事情,序列裏面任意調換兩個字符串的順序後,都會產生更大的字典序。
因爲傳遞性:a.m1<=m1.a
一路下去,而後緊貼着b,b再和a交換。一路下來都是大於等於,那麼確定就只有比原始序列大。(因此原始序列是最經濟的)
這是一個貪心策略從提出策略,到證實正確,要付出較大的心血。
因此用對數器來驗證他對不對便可。
題目實現代碼:
public class Code_05_LowestLexicography { public static class MyComparator implements Comparator<String> { @Override public int compare(String a, String b) { return (a + b).compareTo(b + a); } } public static String lowestString(String[] strs) { if (strs == null || strs.length == 0) { return ""; } //按照對比器排序 Arrays.sort(strs, new MyComparator()); String res = ""; for (int i = 0; i < strs.length; i++) { res += strs[i]; } return res; } public static void main(String[] args) { String[] strs1 = { "jibw", "ji", "jp", "bw", "jibw" }; System.out.println(lowestString(strs1)); String[] strs2 = { "ba", "b" }; System.out.println(lowestString(strs2)); } }
一塊金條切成兩半,是須要花費和長度數值同樣的銅板的。好比長度爲20的 金條,無論切成長度多大的兩半,都要花費20個銅板。一羣人想整分整塊金 條,怎麼分最省銅板?
例如,給定數組{10,20,30},表明一共三我的,整塊金條長度爲 10+20+30=60. 金條要分紅10,20,30三個部分。 若是, 先把長度60的金條分紅10和50,花費60 再把長度50的金條分紅20和30,花費50 一共花費110銅板。
可是若是, 先把長度60的金條分紅30和30,花費60 再把長度30金條分紅10和20,花費30 一共花費90銅板。輸入一個數組,返回分割的最小代價。
標準的哈夫曼編碼問題
子節點合併在一塊兒的代價,是加起來的和。
最終要把全部的葉節點,生成一棵樹。
把數組變爲小根堆,而後從堆裏面取出兩個組成樹,在把組成的父結點,放入堆中...(如此反覆)最後代價就是產生的全部非葉節點。(貪心算法,每次從小根堆裏面拿最小的)
順序是從上面往下面切割。
當這種代價,是有子代價累加/乘,一種累什麼的,也可能給一個公式,這都有可能用哈夫曼編碼貪出來。
要具有起概念思想,用到其餘的題目中。
public class Code_02_Less_Money { public static int lessMoney(int[] arr) { //優先隊列是小根堆 PriorityQueue<Integer> pQ = new PriorityQueue<>(); for (int i = 0; i < arr.length; i++) { pQ.add(arr[i]); } int sum = 0; int cur = 0; while (pQ.size() > 1) { cur = pQ.poll() + pQ.poll(); sum += cur; pQ.add(cur); } return sum; } public static class MinheapComparator implements Comparator<Integer> { //輸入參數的順序都爲 5 3 //若是返回 正數(證實後面的比較小) 第二個參數排在前面 5 - 3 = 2 3排在前面 //若是返回 負數(證實後面的比較大) 第一個參數排在前面 3 - 5 = -2 5排在前面 //0表明兩個東西同樣大 //總結:負1、正二(解決的是哪一個參數排在前面) @Override public int compare(Integer o1, Integer o2) { return o1 - o2; // < 0 o1 < o2 負數 } } public static class MaxheapComparator implements Comparator<Integer> { //等因而-(o1-o2) = o2 - o1 @Override public int compare(Integer o1, Integer o2) { return o2 - o1; // < o2 < o1 } } public static void main(String[] args) { // solution int[] arr = { 6, 7, 8, 9 }; System.out.println(lessMoney(arr)); int[] arrForHeap = { 3, 5, 2, 7, 0, 1, 6, 4 }; // min heap PriorityQueue<Integer> minQ1 = new PriorityQueue<>(); for (int i = 0; i < arrForHeap.length; i++) { minQ1.add(arrForHeap[i]); } while (!minQ1.isEmpty()) { System.out.print(minQ1.poll() + " "); } System.out.println(); // min heap use Comparator PriorityQueue<Integer> minQ2 = new PriorityQueue<>(new MinheapComparator()); for (int i = 0; i < arrForHeap.length; i++) { minQ2.add(arrForHeap[i]); } while (!minQ2.isEmpty()) { System.out.print(minQ2.poll() + " "); } System.out.println(); // max heap use Comparator PriorityQueue<Integer> maxQ = new PriorityQueue<>(new MaxheapComparator()); for (int i = 0; i < arrForHeap.length; i++) { maxQ.add(arrForHeap[i]); } while (!maxQ.isEmpty()) { System.out.print(maxQ.poll() + " "); } } }
輸入: 參數1,正數數組costs 參數2,正數數組profits 參數3,正數k 參數4,正數m
costs[i]表示i號項目的花費 profits[i]表示i號項目在扣除花費以後還能掙到的錢(利潤) k表示你不能並行、只能串行的最多作k個項目 m表示你初始的資金
說明:你每作完一個項目,立刻得到的收益,能夠支持你去作下一個 項目。
輸出: 你最後得到的最大錢數。
兩個數組cost和profit,經過下標列出每一個項目的花費和利潤,作完項目後所得是cost+forfit,假設有資金W,一次只能作一個項目,最多作K個項目。輸出得到最大的錢數。
貪心策略。
首先項目類型包括花費和利潤,再根據花費放入小根堆中,而後看初始資金,在小根堆裏面依次彈出頭部,只要花費比W低的所有彈出。而後根據收益放入大根堆中,而後作大根堆的第一個項目。
當初始資金增長了,再看看小根堆中哪些項目能夠作的,繼續扔到大根堆中。一直作K個結束(或者大根堆裏面沒項目可作)。
public class Code_03_IPO { public static class Node { public int p; public int c; public Node(int p, int c) { this.p = p; this.c = c; } } public static class MinCostComparator implements Comparator<Node> { @Override public int compare(Node o1, Node o2) { return o1.c - o2.c; } } public static class MaxProfitComparator implements Comparator<Node> { @Override public int compare(Node o1, Node o2) { return o2.p - o1.p; } } public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) { Node[] nodes = new Node[Profits.length]; for (int i = 0; i < Profits.length; i++) { nodes[i] = new Node(Profits[i], Capital[i]); } PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator()); PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator()); for (int i = 0; i < nodes.length; i++) { minCostQ.add(nodes[i]); } for (int i = 0; i < k; i++) { //先把能作的項目存在大根堆中 while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) { maxProfitQ.add(minCostQ.poll()); } if (maxProfitQ.isEmpty()) { return W; } W += maxProfitQ.poll().p; } return W; } }
一個數據流中,隨時能夠取得中位數(以前講過,初級2 020143)
不用堆的話,就用一個容器,吐出一個數就裝一個,由於無序的因此裝的時候也是無序的。當想要中位數的時候,要排個序,犧牲大。
準備兩個堆,一個大根堆一個小根堆。
第一個數進入大根堆,下一個數小於等於就進大根堆。
若是從大根堆彈出一個數,就拿最後的一個數和第一個數交換,而後heapsize界限-1。
看題目怎麼作:
先把數加入到大根堆中,若是發現兩個堆大小差值超過了1,就把較大的彈出一個放到另一個去。
若是要查中位數,大根堆堆頂和小根堆堆頂確定能計算出來,由於
大根堆收集了較小的2分之N個,堆頂是其中的最大值。
小根堆收集的是較大的2分之N個,堆頂是其中的最小值。
正好卡在中間的位置。
只要當前數不是小於等於大根堆的,就扔到小根堆中。
策略是固定的:
一、若是當前數小於等於大根堆的堆頂,到大根堆,不然小根堆
二、不平衡了拿出扔到少的堆裏面。
這樣隨時能夠拿到中位數。
堆很好用(調整隻和層數相關、o(logn)),幾乎搞定全部貪心的題目。(面試也常常考)
優先級隊列就是堆。
public class Code_04_MadianQuick { public static class MedianHolder { private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator()); private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator()); private void modifyTwoHeapsSize() { if (this.maxHeap.size() == this.minHeap.size() + 2) { this.minHeap.add(this.maxHeap.poll()); } if (this.minHeap.size() == this.maxHeap.size() + 2) { this.maxHeap.add(this.minHeap.poll()); } } public void addNumber(int num) { if (this.maxHeap.isEmpty()) { this.maxHeap.add(num); return; } if (this.maxHeap.peek() >= num) { this.maxHeap.add(num); } else { if (this.minHeap.isEmpty()) { this.minHeap.add(num); return; } if (this.minHeap.peek() > num) { this.maxHeap.add(num); } else { this.minHeap.add(num); } } modifyTwoHeapsSize(); } public Integer getMedian() { int maxHeapSize = this.maxHeap.size(); int minHeapSize = this.minHeap.size(); if (maxHeapSize + minHeapSize == 0) { return null; } Integer maxHeapHead = this.maxHeap.peek(); Integer minHeapHead = this.minHeap.peek(); //n & 1 判斷奇偶數 //n&1是n和1作「按位與」運算 //1的二進制只有末位是1,因此n&1就是隻保留n的末位(二進制).n&1就表示了n的奇偶性. if (((maxHeapSize + minHeapSize) & 1) == 0) {//兩個堆的數量相同 //若是從數據流中讀出偶數個數值,那麼中位數就是全部數值排序以後中間兩個數的平均值。 return (maxHeapHead + minHeapHead) / 2; } //若是大/小根堆數量多中間數就在大/小根堆的堆頂。 return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead; } } public static class MaxHeapComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { if (o2 > o1) { return 1; } else { return -1; } } } public static class MinHeapComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { if (o2 < o1) { return 1; } else { return -1; } } } // for test public static int[] getRandomArray(int maxLen, int maxValue) { int[] res = new int[(int) (Math.random() * maxLen) + 1]; for (int i = 0; i != res.length; i++) { res[i] = (int) (Math.random() * maxValue); } return res; } // for test, this method is ineffective but absolutely right public static int getMedianOfArray(int[] arr) { int[] newArr = Arrays.copyOf(arr, arr.length); Arrays.sort(newArr); int mid = (newArr.length - 1) / 2; if ((newArr.length & 1) == 0) { return (newArr[mid] + newArr[mid + 1]) / 2; } else { return newArr[mid]; } } public static void printArray(int[] arr) { for (int i = 0; i != arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } public static void main(String[] args) { boolean err = false; int testTimes = 200000; for (int i = 0; i != testTimes; i++) { int len = 30; int maxValue = 1000; int[] arr = getRandomArray(len, maxValue); MedianHolder medianHold = new MedianHolder(); for (int j = 0; j != arr.length; j++) { medianHold.addNumber(arr[j]); } if (medianHold.getMedian() != getMedianOfArray(arr)) { err = true; printArray(arr); break; } } System.out.println(err ? "Oops..what a fuck!" : "today is a beautiful day^_^"); } }
一些項目要佔用一個會議室宣講,會議室不能同時容納兩個項目的宣講。給你每個項目開始的時間和結束的時間(給你一個數組,裏面是一個個具體的項目),你來安排宣講的日程,要求會議室進行的宣講的場次最多。返回這個最多的宣講場次。
安排最先開始的先進行,是不行的,若是那項目持續一天,其餘項目都講不來了了。
按照持續時間短來安排,也得不到最優解。若是一個小項目卡在兩個大項目中間,阻礙了大項目的安排,也不是最優解。
根據哪一個項目早結束安排。
public class Code_06_BestArrange { public static class Program { public int start; public int end; public Program(int start, int end) { this.start = start; this.end = end; } } public static class ProgramComparator implements Comparator<Program> { @Override public int compare(Program o1, Program o2) { return o1.end - o2.end; } } public static int bestArrange(Program[] programs, int start) { Arrays.sort(programs, new ProgramComparator()); int result = 0; for (int i = 0; i < programs.length; i++) { if (start <= programs[i].start) { result++; start = programs[i].end; } } return result; } public static void main(String[] args) { } }
貪心策略就是經驗式的東西(依靠累計)。不用糾結於證實。