你們好,我是小羽。c++
對於編程來講的話,只有掌握了算法纔是瞭解了編程的靈魂,算法對於新手來講的話,屬實有點難度,可是之後想有更好的發展,獲得更好的進階的話,對算法進行系統的學習是重中之重的。程序員
對於 Java 程序員來講,這一門後端語言只是咱們的外功,咱們更多的是學習它的語法,框架以及一些工具的使用。而算法纔是咱們真正的內功,它更多的是關注如何設計系統,如何編寫高性能的代碼,不斷培養咱們的思惟能力,從而提高咱們的工做效率。面試
小羽今天爲你們介紹的是關於 Java 中咱們須要瞭解的一些經典算法,但願你們能從這些經典算法中,品嚐到算法的美妙與奇特,對她產生興趣,更好的爲咱們的職業發展助力前行。好了,開始進入咱們的正文:算法
二分查找
簡介
基本思想:又叫折半查找,要求待查找的序列有序,是一種快速查找算法,時間複雜度爲 O(logn),要求數據集爲一個有序數據集。shell
使用
應用場景:通常用於查找數組元素,而且數組在查找以前必須已經排好序(通常是升序)。編程
步驟:後端
一、取中間位置的值與待查關鍵字比較,若是中間位置的值比待查關鍵字大,則在前半部分循環這個查找的過程,設計模式
二、若是中間位置的值比待查關鍵字小,則在後半部分循環這個查找的過程。數組
三、直到查找到了爲止,不然序列中沒有待查的關鍵字。緩存
代碼示例:
public static int biSearch(int []array,int a){ int lo=0; int hi=array.length-1; int mid; while(lo<=hi){ mid=(lo+hi)/2;//中間位置 if(array[mid]==a){ return mid+1; }else if(array[mid]<a){ //向右查找 lo=mid+1; }else{ //向左查找 hi=mid-1; } } return -1; }
冒泡排序算法
簡介
基本思想:比較先後相鄰的兩個數據,若是前面數據大於後面的數據,就將這二個數據交換。這樣對數組的第 0 個數據到 N-1 個數據進行一次遍歷後,最大的一個數據就「沉」到數組第 N-1 個位置。N=N-1,若是 N 不爲 0 就重複前面二步,不然排序完成。
使用
應用場景:數據量不大,對穩定性有要求,且數據基本有序的狀況。
步驟:
一、將序列中全部元素兩兩比較,將最大的放在最後面。
二、將剩餘序列中全部元素兩兩比較,將最大的放在最後面。
三、重複第二步,直到只剩下一個數。
代碼示例:
public static void bubbleSort1(int [] a, int n){ int i, j; for(i=0; i<n; i++){//表示 n 次排序過程。 for(j=1; j<n-i; j++){ if(a[j-1] > a[j]){//前面的數字大於後面的數字就交換 //交換 a[j-1]和 a[j] int temp; temp = a[j-1]; a[j-1] = a[j]; a[j]=temp; } } } }
插入排序算法
簡介
基本思想:經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應的位置並插入。
使用
應用場景:數據量不大,對算法的穩定性有要求,且數據局部或者總體有序的狀況。
步驟:
一、將第一待排序序列第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
二、從頭至尾依次掃描未排序序列,將掃描到的每一個元素插入有序序列的適當位置。(若是待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)
代碼示例:
public class InsertSort implements IArraySort { @Override public int[] sort(int[] sourceArray) throws Exception { // 對 arr 進行拷貝,不改變參數內容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); // 從下標爲1的元素開始選擇合適的位置插入,由於下標爲0的只有一個元素,默認是有序的 for (int i = 1; i < arr.length; i++) { // 記錄要插入的數據 int tmp = arr[i]; // 從已經排序的序列最右邊的開始比較,找到比其小的數 int j = i; while (j > 0 && tmp < arr[j - 1]) { arr[j] = arr[j - 1]; j--; } // 存在比其小的數,插入 if (j != i) { arr[j] = tmp; } } return arr; } }
快速排序算法
簡介
基本思想:選擇一個關鍵值做爲基準值。比基準值小的都在左邊序列(通常是無序的),比基準值大的都在右邊(通常是無序的)。通常選擇序列的第一個元素。
使用
應用場景:數值範圍較大,相同值的機率較小,數據量大且不考慮穩定性的狀況,數值遠大於數據量時威力更大。
步驟:
一、一次循環,從後往前比較,用基準值和最後一個值比較,若是比基準值小的交換位置,若是沒有繼續比較下一個,直到找到第一個比基準值小的值才交換。
二、找到這個值以後,又從前日後開始比較,若是有比基準值大的,交換位置,若是沒有繼續比較下一個,直到找到第一個比基準值大的值才交換。
三、直到從前日後的比較索引 >
從後往前比較的索引,結束第一次循環,此時,對於基準值來講,左右兩邊就是有序的了。
代碼示例:
public void sort(int[] a,int low,int high){ int start = low; int end = high; int key = a[low]; while(end>start){ //從後往前比較 while(end>start&&a[end]>=key) //若是沒有比關鍵值小的,比較下一個,直到有比關鍵值小的交換位置,而後又從前日後比較 end--; if(a[end]<=key){ int temp = a[end]; a[end] = a[start]; a[start] = temp; } //從前日後比較 while(end>start&&a[start]<=key) //若是沒有比關鍵值大的,比較下一個,直到有比關鍵值大的交換位置 start++; if(a[start]>=key){ int temp = a[start]; a[start] = a[end]; a[end] = temp; } //此時第一次循環比較結束,關鍵值的位置已經肯定了。左邊的值都比關鍵值小,右邊的值都比關鍵值大,可是兩邊的順序還有多是不同的,進行下面的遞歸調用 } //遞歸 if(start>low) sort(a,low,start-1);//左邊序列。第一個索引位置到關鍵值索引-1 if(end<high) sort(a,end+1,high);//右邊序列。從關鍵值索引+1 到最後一個 } }
希爾排序算法
簡介
基本思想:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。
使用
應用場景:數據量較大,不要求穩定性的狀況。
步驟:
一、選擇一個增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;
二、按增量序列個數 k,對序列進行 k 趟排序;
三、每趟排序,根據對應的增量 ti,將待排序列分割成若干長度爲 m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。
代碼示例:
private void shellSort(int[] a) { int dk = a.length/2; while( dk >= 1 ){ ShellInsertSort(a, dk); dk = dk/2; } } private void ShellInsertSort(int[] a, int dk) { //相似插入排序,只是插入排序增量是 1,這裏增量是 dk,把 1 換成 dk 就能夠了 for(int i=dk;i<a.length;i++){ if(a[i]<a[i-dk]){ int j; int x=a[i];//x 爲待插入元素 a[i]=a[i-dk]; for(j=i-dk; j>=0 && x<a[j];j=j-dk){ //經過循環,逐個後移一位找到要插入的位置。 a[j+dk]=a[j]; } a[j+dk]=x;//插入 } } }
歸併排序算法
簡介
基本思想:歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列。
場景使用
應用場景:內存少的時候使用,能夠進行並行計算的時候使用。
步驟:
一、選擇相鄰兩個數組成一個有序序列。
二、選擇相鄰的兩個有序序列組成一個有序序列。
三、重複第二步,直到所有組成一個有序序列。
代碼示例:
public class MergeSortTest { public static void main(String[] args) { int[] data = new int[] { 5, 3, 6, 2, 1, 9, 4, 8, 7 }; print(data); mergeSort(data); System.out.println("排序後的數組:"); print(data); } public static void mergeSort(int[] data) { sort(data, 0, data.length - 1); } public static void sort(int[] data, int left, int right) { if (left >= right) return; // 找出中間索引 int center = (left + right) / 2; // 對左邊數組進行遞歸 sort(data, left, center); // 對右邊數組進行遞歸 sort(data, center + 1, right); // 合併 merge(data, left, center, right); print(data); } /** * 將兩個數組進行歸併,歸併前面 2 個數組已有序,歸併後依然有序 * @param data * 數組對象 * @param left * 左數組的第一個元素的索引 * @param center * 左數組的最後一個元素的索引,center+1 是右數組第一個元素的索引 * @param right * 右數組最後一個元素的索引 */ public static void merge(int[] data, int left, int center, int right) { // 臨時數組 int[] tmpArr = new int[data.length]; // 右數組第一個元素索引 int mid = center + 1; // third 記錄臨時數組的索引 int third = left; // 緩存左數組第一個元素的索引 int tmp = left; while (left <= center && mid <= right) { // 從兩個數組中取出最小的放入臨時數組 if (data[left] <= data[mid]) { tmpArr[third++] = data[left++]; } else { tmpArr[third++] = data[mid++]; } } // 剩餘部分依次放入臨時數組(實際上兩個 while 只會執行其中一個) while (mid <= right) { tmpArr[third++] = data[mid++]; } while (left <= center) { tmpArr[third++] = data[left++]; } // 將臨時數組中的內容拷貝回原數組中 // (原 left-right 範圍的內容被複制回原數組) while (tmp <= right) { data[tmp] = tmpArr[tmp++]; } } public static void print(int[] data) { for (int i = 0; i < data.length; i++) { System.out.print(data[i] + "\t"); } System.out.println(); } }
桶排序算法
簡介
基本思想: 把數組 arr 劃分爲 n 個大小相同子區間(桶),每一個子區間各自排序,最後合併 。計數排序是桶排序的一種特殊狀況,能夠把計數排序當成每一個桶裏只有一個元素的狀況。
使用
應用場景:在數據量很是大,而空間相對充裕的時候是很實用的,能夠大大下降算法的運算數量級。
步驟:
一、找出待排序數組中的最大值 max、最小值 min
二、咱們使用動態數組 ArrayList 做爲桶,桶裏放的元素也用 ArrayList 存儲。桶的數量爲(maxmin)/arr.length+1
三、遍歷數組 arr,計算每一個元素 arr[i] 放的桶
四、每一個桶各自排序
代碼示例:
public static void bucketSort(int[] arr){ int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; for(int i = 0; i < arr.length; i++){ max = Math.max(max, arr[i]); min = Math.min(min, arr[i]); } //建立桶 int bucketNum = (max - min) / arr.length + 1; ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum); for(int i = 0; i < bucketNum; i++){ bucketArr.add(new ArrayList<Integer>()); } //將每一個元素放入桶 for(int i = 0; i < arr.length; i++){ int num = (arr[i] - min) / (arr.length); bucketArr.get(num).add(arr[i]); } //對每一個桶進行排序 for(int i = 0; i < bucketArr.size(); i++){ Collections.sort(bucketArr.get(i)); } }
基數排序算法
簡介
基本思想:將全部待比較數值(正整數)統一爲一樣的數位長度,數位較短的數前面補零。而後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成之後,數列就變成一個有序序列。
使用
應用場景:用於大量數,很長的數進行排序時的狀況。
步驟:
一、將全部的數的個位數取出,按照個位數進行排序,構成一個序列。
二、將新構成的全部的數的十位數取出,按照十位數進行排序,構成一個序列。
代碼示例:
public class radixSort { inta[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,101,56,17,18,23,34,15,35,25,53,51}; public radixSort(){ sort(a); for(inti=0;i<a.length;i++){ System.out.println(a[i]); } } public void sort(int[] array){ //首先肯定排序的趟數; int max=array[0]; for(inti=1;i<array.length;i++){ if(array[i]>max){ max=array[i]; } } int time=0; //判斷位數; while(max>0){ max/=10; time++; } //創建 10 個隊列; List<ArrayList> queue=newArrayList<ArrayList>(); for(int i=0;i<10;i++){ ArrayList<Integer>queue1=new ArrayList<Integer>(); queue.add(queue1); } //進行 time 次分配和收集; for(int i=0;i<time;i++){ //分配數組元素; for(intj=0;j<array.length;j++){ //獲得數字的第 time+1 位數; int x=array[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10, i); ArrayList<Integer>queue2=queue.get(x); queue2.add(array[j]); queue.set(x, queue2); } int count=0;//元素計數器; //收集隊列元素; for(int k=0;k<10;k++){ while(queue.get(k).size()>0){ ArrayList<Integer>queue3=queue.get(k); array[count]=queue3.get(0); queue3.remove(0); count++; } } } } }
剪枝算法
簡介
基本思想:在搜索算法中優化中,剪枝,就是經過某種判斷,避免一些沒必要要的遍歷過程,形象的說,就是剪去了搜索樹中的某些「枝條」,故稱剪枝。應用剪枝優化的核心問題是設計剪枝判斷方法,即肯定哪些枝條應當捨棄,哪些枝條應當保留的方法。
使用
應用場景:一般應用在 DFS
和 BFS
搜索算法中,尋找過濾條件,提早減小沒必要要的搜索路徑。
步驟:
一、基於訓練數據集生成決策樹,生成的決策樹要儘可能大;
二、用驗證數據集最已生成的樹進行剪枝並選擇最優子樹,這時用損失函數最小做爲剪枝的標準
代碼示例:
class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { Arrays.sort(candidates); LinkedList<Integer> track = new LinkedList<>(); combinationSum(candidates, 0, target, track); return result; } private List<List<Integer>> result = new ArrayList<>(); private void combinationSum(int[] candidates, int start, int target, LinkedList<Integer> track) { if (target < 0) { return; } else if (target == 0) { result.add(new LinkedList<>(track)); return; } for (int i = start; i < candidates.length; i++) { if (target < candidates[i]) { break; } track.add(candidates[i]); combinationSum(candidates, i, target - candidates[i], track); track.removeLast(); } } }
回溯算法
簡介
基本思想:回溯算法實際上一個相似枚舉的搜索嘗試過程,主要是在搜索嘗試過程當中尋找問題的解,當發現已不知足求解條件時,就「回溯」返回,嘗試別的路徑。
使用
應用場景:設置一個遞歸函數,函數的參數會攜帶一些當前的可能解的信息,根據這些參數得出可能解或者不可能而回溯,平時常常見的有 N 皇后、數獨、集合等狀況。
步驟:
一、定義一個解空間,它包含問題的解;
二、利用適於搜索的方法組織解空間;
三、利用深度優先法搜索解空間;
四、利用限界函數避免移動到不可能產生解的子空間。
代碼示例:
function backtrack(n, used) { // 判斷輸入或者狀態是否非法 if (input/state is invalid) { return; } // 判讀遞歸是否應當結束,知足結束條件就返回結果 if (match condition) { return some value; } // 遍歷當前全部可能出現的狀況,並嘗試每一種狀況 for (all possible cases) { // 若是上一步嘗試會影響下一步嘗試,須要寫入狀態 used.push(case) // 遞歸進行下一步嘗試,搜索該子樹 result = backtrack(n + 1, used) // 在這種狀況下已經嘗試完畢,重置狀態,以便於下面的回溯嘗試 used.pop(case) } }
最短路徑算法
簡介
基本思想:從某頂點出發,沿圖的邊到達另外一頂點所通過的路徑中,各邊上權值之和最小的一條路徑叫作最短路徑。解決最短路的問題有如下算法,Dijkstra 算法,Bellman-Ford 算法,Floyd 算法和 SPFA 算法等。
使用
應用場景:應用有計算機網絡路由算法,機器人探路,交通路線導航,人工智能,遊戲設計等。
步驟:(Dijkstra 算法示例)
一、 訪問路網中裏起始點最近且沒有被檢查過的點,把這個點放入 OPEN 組中等待檢查。
二、 從OPEN表中找出距起始點最近的點,找出這個點的全部子節點,把這個點放到 CLOSE 表中。
三、 遍歷考察這個點的子節點。求出這些子節點距起始點的距離值,放子節點到 OPEN 表中。
四、重複2,3,步。直到 OPEN 表爲空,或找到目標點。
代碼示例:
//Dijkstra 算法 static int[] pathSrc = new int[9]; static int[] shortPath = new int[9]; static void shortestPath_DijkStra(MGraph m, int v0) { // finalPath[w] = 1 表示已經獲取到頂點V0到Vw的最短路徑 int[] finalPath = new int[9]; for (int i = 0; i < m.numVertexes; i++) { finalPath[i] = 0; shortPath[i] = m.arc[v0][i]; pathSrc[i] = 0; } // v0到v0的路徑爲0 shortPath[v0] = 0; // vo到v0不須要求路徑 finalPath[v0] = 1; for (int i = 1; i < m.numVertexes; i++) { // 當前所知的離V0最近的距離 int min = INFINITY; int k = 0; for (int w = 0; w < m.numVertexes; w++) { if(shortPath[w] < min && finalPath[w] == 0) { min = shortPath [w]; k = w; } } finalPath[k] = 1; // 修改finalPath的值,標記爲已經找到最短路徑 for (int w = 0; w < m.numVertexes; w++) { // 若是通過V頂點的路徑比原來的路徑(不通過V)短的話 if(finalPath[w] == 0 && (min + m.arc[k][w]) < shortPath[w]) { // 說明找到了更短的路徑,修改 shortPath[w] = min + m.arc[k][w]; // 修改路徑的長度 pathSrc[w] = k; // 修改頂點下標W的前驅頂點 } } } }
最大子數組算法
簡介
基本思想:給定一個整數數組 nums ,找到一個具備最大和的連續子數組(子數組最少包含一個元素),返回其最大和。
使用
應用場景:生活中能夠用來查看股票一週以內的增加狀態,須要獲得最合適的買入和賣出時間。
步驟:
一、將子串和爲負數的子串丟掉,只留和爲正的子串。
二、若是 nums 中有正數,從左到右遍歷 nums,用變量 cur 記錄每一步的累加和,遍歷到正數 cur 增長,遍歷到負數 cur 減小。
三、當 cur>=0 時,每一次累加均可能是最大的累加和,因此,用另一個變量 max 全程跟蹤記錄 cur 出現的最大值便可。
代碼示例:
class Solution { public: /* * @param nums: A list of integers * @return: A integer indicate the sum of max subarray */ int maxSubArray(vector<int> nums) { if(nums.size()<=0){ return 0; } int max=INT_MIN,cur=0;//c++最小值 for(int i=0; i<nums.size(); i++) { if(cur < 0) cur = nums[i];//若是前面加起來的和小於0,拋棄前面的 else cur+=nums[i]; if(cur > max) max = cur; } return max; } };
最長公共子序算法
簡介
基本思想:最長公共子序列是一個在一個序列集合中用來查找全部序列中最長子序列的問題。這與查找最長公共子串的問題不一樣的地方是:子序列不須要在原序列中佔用連續的位置。
使用
應用場景:最長公共子序列問題是一個經典的計算機科學問題,也是數據比較程序,好比 Diff
工具,和生物信息學應用的基礎。它也被普遍地應用在版本控制,好比 Git 用來調和文件之間的改變。
步驟:
一、可使用遞歸去解決,須要遍歷出全部的可能,很慢;
二、對於通常的 LCS 問題,都屬於 NP 問題;
三、當數列的量爲必定的時,均可以採用動態規劃去解決。
代碼示例:
class Solution { public int longestCommonSubsequence(String text1, String text2) { int length1 = text1.length(); int length2 = text2.length(); int[][] a = new int[length1 + 1][length2 + 1];//0行0列保留 for(int i = 1; i <= length1; i++){ for(int j = 1; j <= length2; j++){ if (text1.charAt(i - 1) == text2.charAt(j - 1)) { a[i][j] = a[i - 1][j - 1] + 1; } else { if (a[i][j - 1] > a[i-1][j]) { a[i][j] = a[i][j - 1]; } else { a[i][j] = a[i - 1][j]; } } } } return a[length1][length2]; } }
最小生成樹算法
簡介
基本思想:在含有n個頂點的帶權無向連通圖中選擇n-1條邊,構成一棵極小連通子圖,並使該連通子圖中n-1條邊上權值之和達到最小,則稱其爲連通網的最小生成樹(不必定惟一)。
通常狀況,要解決最小生成樹問題,一般採用兩種算法:Prim算法和Kruskal算法。
使用
應用場景:通常用來計算成本最小化的狀況。
步驟:(Prim 算法示例)
一、以某一個點開始,尋找當前該點能夠訪問的全部的邊;
二、在已經尋找的邊中發現最小邊,這個邊必須有一個點尚未訪問過,將尚未訪問的點加入咱們的集合,記錄添加的邊;
三、尋找當前集合能夠訪問的全部邊,重複 2 的過程,直到沒有新的點能夠加入;
四、此時由全部邊構成的樹即爲最小生成樹。
代碼示例:
/** prim算法 * @param first 構成最小生成樹的起點的標識 * @return 返回最小生成樹構成的邊 */ public List<Edge> generateMinTreePrim(T first){ //存儲最小生成樹構成的邊 List<Edge> result=new LinkedList<>(); //首先創建map,key爲vertex,value爲edge HashMap<Vertex<T>, Edge> map=new HashMap<>(); Iterator<Vertex<T>> vertexIterator=getVertexIterator(); Vertex<T> vertex; Edge edge; while(vertexIterator.hasNext()){ //一開始,value爲edge的兩端的都爲本身,weight=maxDouble vertex=vertexIterator.next(); edge=new Edge(vertex, vertex, Double.MAX_VALUE); map.put(vertex, edge); } //first是構成最小生成樹的起點的標識 vertex=vertexMap.get(first); if(vertex==null){ System.out.println("沒有節點:"+first); return result; } //全部不在生成樹中的節點,都是map的key,若是map爲空,表明全部節點都在樹中 while(!map.isEmpty()){ //此次循環要加入生成樹的節點爲vertex,邊爲vertex對應的edge(也就是最小的邊) edge=map.get(vertex); //每將一個結點j加入了樹A,首先從map中去除這個節點 map.remove(vertex); result.add(edge); System.out.println("生成樹加入邊,頂點:"+vertex.getLabel()+ " ,邊的終點是:"+edge.getEndVertex().getLabel()+" ,邊的權值爲: "+edge.getWeight());; //若是是第一個節點,對應的邊是到本身的,刪除 if(vertex.getLabel().equals(first)){ result.remove(edge); } //而後看map中剩餘的節點到節點j的距離,若是這個邊的距離小於以前邊的距離,就將邊替換成這個到節點j的邊 //在遍歷替換中,同時發現距離最短的邊minEdge Edge minEdge=new Edge(vertex, vertex, Double.MAX_VALUE); for(Vertex<T> now:map.keySet()){ edge=map.get(now); //newEdge爲now到節點j的邊 Edge newEdge=now.hasNeighbourVertex(vertex); if(newEdge!=null&&newEdge.getWeight()<edge.getWeight()){ //若是這個邊的距離小於以前邊的距離,就將邊替換成這個到節點j的邊 edge=newEdge; map.put(now, edge); } if(edge.getWeight()<minEdge.getWeight()){ //更新minEdge minEdge=edge; } } //這裏設定邊的方向是不在樹上的v(爲起始點)到樹上的u //這條邊的起始點是不在樹上的,是下一個加入生成樹的節點 vertex=minEdge.getBeginVertex(); } return result; }
最後
算法不管是對於學習仍是工做,都是必不可少的。若是說咱們掌握了這些算法背後的邏輯思想,那麼是會對咱們的學習和工做有很好的促進做用的。
其次算法對於面試,尤爲是進入一些大廠 BAT 等公司都是一塊敲門磚,大公司都會經過算法來評估你的總體技術水平,若是你有很好的算法功底,相信對你將來的職場道路也會有很大幫助。
在職業發展後期,擁有良好的算法技能,能夠幫助咱們更快、更高效的完成編碼,往架構師的方向發展,一樣的崗位,你有相應的算法知識的話,能拿到的薪資也會比別人更好一點。
固然,算法遠不止這些羅列的,還有不少複雜的算法須要去不斷學習,一塊兒加油吧~
推薦閱讀
藏在成都這個陰雨小城裏的互聯網公司
【硬核】23種設計模式娓娓道來,助你優雅的編寫出漂亮代碼!
微服務面試必問的Dubbo,這麼詳細還怕本身找不到工做?
本文分享自微信公衆號 - 淺羽的IT小屋(QY18804079159)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。