2019校招Android面試題解1.0(演算法篇)
在校招題解的演算法篇中,還整理了部分《劍指offer》原題,這裡均用Java實現。
- 校招面試題解
- 劍指offer題解(部分)
1.校招面試題解
注:題目源於 ofollow,noindex">2019Android秋招提前批面試總結 之 資料結構&演算法
Q:怎麼理解資料結構?
- 技術點:資料結構
- 思路:資料結構的定義、分類
- 參考回答:研究資料的邏輯結構和物理結構以及它們之間相互關係,並對這種結構定義相應的運算,而且確保經過這些運算後所得到的新結構仍然是原來的結構型別。
- 按照邏輯結構分類
- 線性結構:線性表、棧、佇列
- 非線性結構:樹、圖
- 按照儲存結構分為順序結構、鏈式結構、索引結構、雜湊結構
- 按照邏輯結構分類
Q:什麼是斐波那契數列?
- 技術點:遞迴和迴圈
- 思路:斐波那契數列的定義
- 參考回答:斐波那契數列指的是這樣的數列1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...即這個數列從第3項開始,每一項都等於前兩項之和,數學表示F(1)=1,F(2)=1, F(3)=2,F(n)=F(n-1)+F(n-2)(n>=4,n∈N*)
Q:迭代和遞迴的特點,並比較優缺點
- 技術點:遞迴和迴圈
- 參考回答:遞迴和迭代都是迴圈的一種,特點:
- 遞迴就是通過重複呼叫函式自身實現迴圈;滿足終止條件時會逐層返回來結束迴圈
- 迭代通過函式內某段程式碼實現迴圈;使用計數器結束迴圈
優點 | 缺點 | |
---|---|---|
迭代 | 程式碼更簡潔清晰,可讀性更好 | 需要呼叫函式,會造成空間的浪費;使用棧機制,迴圈次數太多易造成堆疊溢位 |
遞迴 | 效率高;無額外開銷,節省空間 | 程式碼不如遞迴簡潔 |
Q:瞭解哪些查詢演算法,時間複雜度都是多少?
- 技術點:查詢
- 參考回答:程式碼詳見 java實現常見查詢演算法
名稱 | 型別 | 特點 | 時間複雜度 | 適用 |
---|---|---|---|---|
順序查詢 | 靜態查詢表 | 從表中第一個(或最後一個)記錄開始,逐個進行記錄的關鍵字和給定值比較 | O(n) | 無序表 |
有序查詢 | 靜態查詢表 | 根據分隔點的選擇不同分以下三種查詢方法: | O(logn) | 有序表 |
1.二分查詢 | 取中間值為比較物件,若等於關鍵字,則查詢成功;若大於關鍵字,則在比較物件的左半區繼續查詢;若小於關鍵字,則在比較物件的右半區繼續查詢。不斷重複上述過程直到查詢成功,若所有查詢區域無記錄則查詢失敗 | |||
2.插值查詢 | 是根據要查詢的關鍵字與查詢表中最大最小記錄的關鍵字比較後的查詢方法,其核心就在於插值的計算公式 (key-a[low])/(a[high]-a[low])*(high-low) | 表長較大而關鍵字分佈比較均勻 | ||
3.斐波那契查詢 | 在二分查詢的基礎上根據斐波那契數列進行分割的 | |||
線性索引查詢 | 靜態查詢表 | 引入索引並將索引項集合組織為線性結構,常用的三種線性索引技術: | 資料量極大並按照先後順序儲存 | |
1.稠密索引 | 資料集中的每個記錄都對應一個索引項,且索引項按照關鍵碼進行有序排列 | |||
2.分塊索引 | 是把資料集的記錄分成了若干塊,塊內無序、塊間有序 | |||
3.倒排索引 | 不是由記錄來確定屬性值,而是由屬性值來確定記錄的位置 | |||
樹表查詢 | 動態查詢表 | 以樹結構儲存資料 | 頻繁進行插入和刪除資料的操作 | |
1.二叉查詢樹 | 左子樹結點一定比其雙親結點小,右子樹結點一定比其雙親結點大 | 最好O(logn)、最壞O(n) | ||
2.平衡二叉樹 | 是一種二叉排序樹,其中每一個節點的左子樹和右子樹的高度差至多等於1 | O(logn) | ||
3.B樹 | 是一種平衡的多路查詢樹(每一個結點的孩子數可以多於兩個且每一個結點處可以儲存多個元素) | 資料集非常大 | ||
3.B+樹 | 是一種B樹的變形樹,將所有葉子結點都連結在一起 | 帶有範圍的查詢 | ||
雜湊查詢 | 通過一個雜湊函式計算出資料元素的儲存地址 | O(1) | 以空間換時間 |
Q:瞭解哪些排序演算法,並比較一下,以及適用場景
- 技術點:排序
- 參考回答:程式碼詳見 十大經典排序演算法最強總結(含JAVA程式碼實現)
(要求排序結果從小到大)
名稱 | 特點 | 時間複雜度 | 空間複雜度 | 穩定性 | 適用 |
---|---|---|---|---|---|
氣泡排序 | 重複走訪要排序的數列,一次比較兩個元素,若較小元素在後則交換,能看到越小的元素會經由交換慢慢浮到數列的頂端 | O(n2) | O(1) | 穩定 | 資料規模較小 |
簡單選擇排序 | 每次都在未排序序列中找最小元素 | O(n2) | O(1) | 穩定 | 資料規模較小且對穩定性有要求 |
直接插入排序 | 對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入 | 最好O(n),平均O(n2) | O(1) | 穩定 | 資料規模較小且待排序列基本有序 |
希爾排序 | 將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序 | O(nlogn)~ O(n2) | O(1) | 不穩定 | 資料規模較大 |
歸併排序 | 先使每個子序列有序,再使子序列段間有序 | O(nlogn) | O(n) | 穩定 | 資料規模較大且對穩定性有要求 |
堆排序 | 近似完全二叉樹的結構,子結點的鍵值或索引總是小於(或大於)其父節點 | O(nlogn) | O(1) | 不穩定 | 資料規模較大,相比快排好處是不會出現最壞情況、需要的輔助空間少 |
快速排序 | 取一個記錄作為樞軸,經過一趟排序將整段序列分為兩個部分,使得數軸左側都小於樞軸、右側都大於樞軸;再對這兩部分繼續進行排序使整個序列達到有序 | 最壞 O(n2),平均O(nlogn) | O(logn)~O(n) | 不穩定 | 資料規模較大且待排序列無序 |
Q:快排的基本思路是什麼?最差的時間複雜度是多少?如何優化?
-
技術點:排序
-
參考回答:快速排序使用分治的思想,通過一趟排序將待排序列分割成兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小;再分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。當待排序列有序時會出現最壞時間複雜度O(n2)。幾種優化方式:
- 當待排序序列的長度較小時採用直接插入排序
- 優化所選取數軸的計算方法,如三數取中
- 迭代取代遞迴,效率高
- 儲存數軸值,節省無必要的交換
Q:二叉排序樹插入或刪除一個節點的過程是怎樣的?
- 技術點:查詢
- 參考回答:
- 二叉排序樹插入操作:先查詢該元素是否存在於二叉排列樹中並記錄其根節點,若沒有則比較其和根節點大小後插入相應位置
- 二叉排序樹刪除操作:
- 待刪除節點是葉子節點,直接刪除即可
- 待刪除節點是僅有左或右子樹的節點 ,上移子樹即可
- 待刪除節點是左右子樹都有的節點 ,用刪除節點的直接前驅或直接後繼來替換當前節點
Q:什麼是紅黑樹?
- 技術點:查詢
- 參考回答:紅黑樹是一種自平衡二叉查詢樹,包含性質:
- 節點是紅色或黑色
- 根節點是黑色
- 葉子節點是黑色
- 每個紅色節點的兩個子節點都是黑色
- 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
Q:100盞燈問題
- 題目說明: 100盞燈問題
- 思路:最後亮著的燈是被拉動次數為奇數次的;由於只有完全平方數的約數個數為奇數,故本題轉換為求100內完全平方數的個數
Q:7瓶水1瓶有毒3只老鼠,怎麼找有毒的水,再加個條件,必須要求第二天出結果
- 技術點:查詢
- 思路:若無時間限制,可採用類似二分查詢的思路;若要求第二天出結果,用二進位制編碼的思路
- 參考回答:
- 二分查詢思路,每次均分兩組,每組各取一滴水混合成新溶劑餵給老鼠,繼續對導致老鼠死亡的一組水進行同上操作。假如是第1瓶有毒,過程演繹如下,第一隻老鼠死於前一堆(mid=(0+6)/2=3,即服用了第1、2、3、4瓶的混合溶劑),第二隻老鼠死於前一堆(mid=(0+3)/2=1,即服用了第1、2瓶的混合溶劑),第三隻老鼠隨意試一瓶,根據服用後狀態即可判斷有毒的水。
- 二進位制編碼思路,對每瓶水二進位制編碼,所需編碼位數正好為三位,將第一位是1的水混為新溶劑餵給第一個老鼠,以此類推,看三隻老鼠服用狀態(死亡=1,存活=0)得出對應的編碼,找到對應的水即可。假如是第1瓶有毒,編碼之後,讓第一隻老鼠服用第4、5、6、7瓶的混合溶劑,第二隻老鼠服用第2、3、6、7瓶的混合溶劑,第三隻老鼠服用第1、3、5、7瓶的混合溶劑,最終第一隻和第二隻老鼠存活,第三隻老鼠死亡,對應編碼為001,對應的水是第一瓶,故此瓶有毒。
**
第一瓶水 001
第二瓶水 010
第三瓶水 011
第四瓶水 100
第五瓶水 101
第六瓶水 110
第七瓶水 111
**
Q:海量資料問題
- 技術點:海量資料問題
- 思路:分治、雜湊、bit、堆
- 參考回答: 海量資料處理面試題集錦
Q:(手寫演算法)二分查詢
- 技術點:查詢
- 參考程式碼:
public static int binarySearch(int[] a, int key) { int low, mid, high; low = 0;//最小下標 high = a.length - 1;//最大小標 while (low <= high) { mid = (high + low) / 2;//折半下標 if (key > a[mid]) { low = mid + 1; //關鍵字比折半值大,則最小下標調成折半下標的下一位 } else if (key < a[mid]) { high = mid - 1;//關鍵字比折半值小,則最大下標調成折半下標的前一位 } else { return mid; //關鍵字和折半值相等時返回折半下標 } } return -1; }
Q:(手寫演算法)反轉連結串列
- 技術點:連結串列
- 思路:
- 方法1:重複將首節點的下一個節點調整到最前面,如連結串列1->2->3->4,調整過程為2->1->3->4,3->2->1->4,4->3->2->1
- 方法2:遞迴,使連結串列從尾節點開始指向前一個節點
- 參考程式碼:
//節點類 public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } } //方法1 public ListNode reverseLinkedList(ListNode head){ if (head == null || head.next == null){ return head; } ListNode p = new ListNode(-1);//擬一個頭節點 p.next = head; ListNode nextNode = head.next; while (nextNode != null){ //後一個節點調整到最前 head.next = nextNode.next; nextNode.next = p.next; p.next = nextNode; nextNode = head.next; } return p.next; } //方法2,遞迴 public ListNode reverseLinkedList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode pNode = reverseLinkedList(head.next); head.next.next = head; head.next = null; return pNode; }
Q:(手寫演算法)用兩個棧實現一個佇列,完成佇列的Push和Pop操作。 佇列中的元素為int型別。
- 技術點:棧和佇列
- 思路:
- 入隊:將元素進棧A
-
出隊:判斷棧B是否為空,如果為空,則將棧A中所有元素pop,並push進棧B,棧B出棧, 反之棧B直接出棧
- 參考程式碼:
public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack<Integer> stack2 = new Stack<Integer>(); //入隊 public void add(int node) { stack1.push(node); } //出隊 public int poll() { if(stack1.empty()&&stack2.empty()){ throw new RuntimeException("Queue is empty!"); } if(stack2.empty()){ while(!stack1.empty()){ stack2.push(stack1.pop()); } } return stack2.pop(); } }
Q:(手寫演算法)用兩個佇列實現一個棧,完成棧的Push和Pop操作。 佇列中的元素為int型別。
- 技術點:棧和佇列
- 思路:
- 入棧:將元素進佇列A
-
出棧:判斷佇列A中元素的個數是否為1,如果等於1則直接出佇列A,否則將佇列A中的元素依次出佇列並放入佇列B、直到佇列A中只剩一個原色,然後佇列A出佇列,再把佇列B中的元素以此出佇列並放入佇列A
- 參考程式碼:
public class Solution { Queue<Integer> queue1 = new ArrayDeque<Integer>(); Queue<Integer> queue2 = new ArrayDeque<Integer>(); //入棧 public void push(int node) { queue1.add(node); } //出棧 public int pop() { if(queue1.isEmpty()&&queue2.isEmpty()){ throw new RuntimeException("Stack is empty!"); } if(queue1.isEmpty()){ while(!queue2.isEmpty()){ queue1.add(queue2.poll()); } } if(queue1.size()!=1){ while(queue1.size()!=1){ queue2.add(queue1.poll()); } } return queue1.poll(); } }
Q:(手寫演算法)用三個執行緒,順序列印字母A-Z,輸出結果是1A、2B、3C、1D 2E...
- 技術點:執行緒同步
- 思路:加鎖進行限制,並配合wait()和notifyAll()
- 參考程式碼:
private static char c = 'A'; private static int i = 0; public static void main(String[] args) { Runnable runnable = new Runnable() { public void run() { synchronized (this) {//加鎖 try { int threadId = Integer.parseInt(Thread.currentThread().getName()); while (i < 26) { if (i % 3 == threadId - 1) { System.out.println(threadId +""+ (char) c++); i++; notifyAll();// 喚醒處於等待狀態的執行緒 } else { wait();// 釋放當前鎖並進入等待狀態 } } } catch (InterruptedException e) { e.printStackTrace(); } }//執行結束釋放當前鎖 } }; Thread t1 = new Thread(runnable, "1"); Thread t2 = new Thread(runnable, "2"); Thread t3 = new Thread(runnable, "3"); t1.start(); t2.start(); t3.start(); }
Q:(手寫演算法)如何判斷一個鏈有環,請找出該連結串列的環的入口結點,否則輸出null
- 技術點:連結串列
- 思路:用p1、p2指向連結串列頭部,然後p1每次走一步、p2每次走兩步;顯然,當p1和p2第一次相遇時,p2正好比p1多走了一個環;設p2此時走了2x步,p1走了x步,則環長=2x-x=x,因此p1和p2相遇點正好是環的入口點,只要找到p1==p2時的節點即可
- 參考程式碼:
//節點類ListNode同上 public class Solution { public ListNode EntryNodeOfLoop(ListNode pHead){ if(pHead == null || pHead.next == null){ return null; } ListNode p1 = pHead; ListNode p2 = pHead; while(p2 != null && p2.next != null ){ p1 = p1.next;//p1每次走一步 p2 = p2.next.next;//p2每次走兩步 if(p1 == p2){ p2 = pHead; while(p1 != p2){ p1 = p1.next; p2 = p2.next; } if(p1 == p2){ return p1;//p1和p2相遇點正好是環的入口點 } } } return null; } }
Q:(手寫演算法)如何判斷兩條鏈交叉
- 技術點:連結串列
- 思路:分別考慮連結串列上是否存在環的情況
- 參考程式碼: JAVA 判斷兩個單鏈表是否相交併求交點
Q:(手寫演算法)快速從一組無序數中找到第k大的數(或前k個大的數)
- 技術點:排序
- 思路:利用快排思想,直至找到一個排在第k位置的樞軸,因為左邊所有資料都比它大,右邊都比它小。
- 參考程式碼:
public class QuickSort { public int partition(int[] arr,int low,int high){ int temp=arr[low]; while(low<high){ while(arr[high]<=temp&&high>low){ high--; } arr[low]=arr[high]; while(arr[low]>=temp&&low<high){ low++; } arr[high]=arr[low]; } arr[high]=temp; return high; } public void findK(int k,int[] arr,int low,int high){ int temp=partition(arr,low,high); if(temp==k-1){ System.out.print("第"+k+"大的數是:"+arr[temp]); }else if(temp>k-1){ findK(k,arr,low,temp-1); }else{ findK(k,arr,temp+1,high); } } }
Q:(手寫演算法)從字串中找出一個最長的不包含重複數字的子字串的長度。例如在字串中”arabcacfr”,最長非重複子字串為“rabc”或”acfr”,長度為4。
- 技術點:字串
- 思路:使用動態規劃。先用長度為26的陣列positions來儲存當前字元上次出現的位置;再定義字串長度的陣列lines表示以當前字母為結尾的最長不含重複字元的子字串的長度。依次遍歷字串中的字元:
- 若當前字元是第一次出現,說明可以直接新增到前一個非重複子字串,因此lines[i]=lines[i-1]+1;
- 若當前字元非第一次出現,需計算當前字元與它上次出現位置之間的距離d:
- 若d大於lines[i-1],說明前一個非重複子字串中沒有包含當前字元,可以添加當前字元到前一個非重複子字串中,因此lines[i]=lines[i-1]+1;
- 若d小於或等於f(i-1),說明如果加入當前字元會存在重複字串,需要把上次出現的字元截開,因此lines[i] = d
- 參考程式碼:
private int findLongestSubstringLength(String string){ if (string == null || string.equals("")) { return 0; } int maxLength = 0;//最長不重複子字串的長度 int[] positions = new int[26];//儲存當前字元上次出現的位置,-1表示沒有出現過 for (int i = 0; i < positions.length; i++){ positions[i] = -1; } int[] lines = new int[string.length()];//儲存以當前字元為尾的最長不重複子字串的長度 lines[0]=1; positions[string.charAt(0) - 'a']=0; for (int i = 1; i < string.length(); i++){ int prePosition = positions[string.charAt(i) - 'a']; if(prePosition>=0){//當前字元非第一次出現 if((i-prePosition)>lines[i-1]){ lines[i]=lines[i-1]+1; }else{ lines[i]=i-prePosition;//若加入當前字元會出現重複,需要截斷 } }else{//當前字元是第一次出現 lines[i]=lines[i-1]+1; } positions[string.charAt(i) - 'a'] = i; if(lines[i]>maxLength){ maxLength=lines[i]; } } return maxLength; }
2.劍指offer題解
注:題目源於 牛客網_劍指Offer
Q:輸入一個連結串列,按連結串列值從尾到頭的順序返回一個ArrayList。
- 技術點:連結串列
- 思路:通過遞迴實現連結串列節點從尾到頭依次插入到ArrayList
- 參考程式碼:
//節點類ListNode同上 import java.util.ArrayList; public class Solution { ArrayList<Integer> arrayList=new ArrayList<Integer>(); public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { if(listNode!=null){ this.printListFromTailToHead(listNode.next);//遞迴 arrayList.add(listNode.val); } return arrayList; } }
Q:在一個排好序的連結串列中存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,並返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5。
- 技術點:連結串列
- 思路:從頭節點開始如果下個節點與當前節點重複,則找到第一個與當前不同的節點開始遞迴,反之,保留當前結點並從下一個結點開始遞迴
- 參考程式碼:
//節點類ListNode同上 public class Solution { public ListNode deleteDuplication(ListNode pHead) { if (pHead == null || pHead.next == null) { return pHead;//停止遞迴條件 } if (pHead.val == pHead.next.val) { //如果下個節點與當前節點重複,則找到第一個與當前不同的節點開始遞迴 ListNode pNode = pHead.next; while (pNode != null && pNode.val == pHead.val) { pNode = pNode.next; } return deleteDuplication(pNode); } else { //反之,保留當前結點,並從下一個結點開始遞迴 pHead.next = deleteDuplication(pHead.next); return pHead; } } }
Q:輸入一個連結串列,輸出該連結串列中倒數第k個結點。
- 技術點:連結串列
- 思路:用p1、p2指向連結串列頭部,先讓p1走(k-1)步到達第k個節點,此時p1和p2相隔k個節點;然後p1、p2同時往後移動,當p1到達鏈尾時,p2所在位置正是倒數第k個節點
- 參考程式碼:
//節點類ListNode同上 public class Solution { public ListNode FindKthToTail(ListNode head,int k) { if(head==null||k<=0){ return null; } ListNode p1=head; ListNode p2=head; //p1先到達第k個節點處 for(int i=1;i<k;i++){ if(p1.next!=null){ p1=p1.next; }else{ return null; } } //p1走到鏈尾時p2正為倒數第k個節點 while(p1.next!=null){ p1=p1.next; p2=p2.next; } return last; } }
Q:輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,並保證滿足單調不減規則。
- 技術點:連結串列
- 思路:每次都取倆連結串列中最靠前的倆元素中的更小元素到重組連結串列中,被取走元素的舊連結串列後移一個元素繼續進行“車輪戰”;注意如果只剩一條未結束連結串列要記得全部插入到重組連結串列的最後
- 參考程式碼:
//方法1:非遞迴 public class Solution { public ListNode Merge(ListNode list1,ListNode list2) { //新建一個頭節點,用來存合併的連結串列 ListNode head=new ListNode(-1); head.next=null; ListNode root=head; while(list1!=null&&list2!=null){ if(list1.val<list2.val){ head.next=list1; head=list1; list1=list1.next; }else{ head.next=list2; head=list2; list2=list2.next; } } //把未結束的連結串列連線到合併後的連結串列尾部 if(list1!=null){ head.next=list1; } if(list2!=null){ head.next=list2; } return root.next; } } //方法2:遞迴 public class Solution { public ListNode Merge(ListNode list1,ListNode list2) { if(list1 == null){ return list2; } if(list2 == null){ return list1; } if(list1.val <= list2.val){ list1.next = Merge(list1.next, list2); return list1; }else{ list2.next = Merge(list1, list2.next); return list2; } } }
Q:給定一個數組和滑動視窗的大小,找出所有滑動窗口裡數值的最大值。假設輸入陣列{2,3,4,2,6,2,5,1}及滑動視窗大小3,那麼一共存在6個滑動視窗: {[2,3,4],2,6,2,5,1}、 {2,[3,4,2],6,2,5,1}、 {2,3,[4,2,6],2,5,1}、 {2,3,4,[2,6,2],5,1}、 {2,3,4,2,[6,2,5],1}、 {2,3,4,2,6,[2,5,1]},且他們的最大值分別為{4,4,6,6,6,5}。
- 技術點:棧和佇列
- 思路:用一個雙端佇列(頭尾都可push和pop)儲存下標值,且隊頭儲存當前視窗最大值的下標。遍歷陣列,先從隊頭開始把不存在當前視窗的都移除佇列;然後從隊尾開始把比當前元素小的都移除佇列;最後把當前元素的下標值插入到佇列中。時間複雜度 O(n) 。
- 參考程式碼:
public class Solution { public ArrayList<Integer> maxInWindows(int [] num, int size) { ArrayList<Integer> res = new ArrayList<>(); if(size == 0){ return res; } int begin; ArrayDeque<Integer> q = new ArrayDeque<>();//雙端佇列 for(int i = 0; i < num.length; i++){ begin = i - size + 1;//表示當前視窗最左元素在原始陣列中的下標 if(q.isEmpty()){ q.add(i); } //從隊頭開始把不存在當前視窗的都移除佇列 while((!q.isEmpty()) &&begin > q.peekFirst()){ q.pollFirst(); } //從隊尾開始把比當前元素小的都移除佇列 while((!q.isEmpty()) && num[q.peekLast()] <= num[i]){ q.pollLast(); } q.add(i); //視窗有效時插入最大值 if(begin >= 0){ res.add(num[q.peekFirst()]); } } return res; } }
Q:定義棧的資料結構,請在該型別中實現一個能夠得到棧中所含最小元素的min函式(時間複雜度為O(1))。
- 技術點:棧
- 思路:自定義一個最小元素棧,進行push操作時, 如果壓入的元素小於等於棧頂元素, 則壓入最小元素棧;進行pop操作時, 如果彈出的元素和棧頂元素相等, 就把最小元素棧頂也彈出。
- 參考程式碼:
public class Solution { Stack<Integer> dataStack = new Stack<Integer>(); Stack<Integer> minStack = new Stack<Integer>(); public void push(int node) { dataStack.push(node); if(minStack.isEmpty() || node <= minStack.peek()){ minStack.push(node); } } public void pop() { if(dataStack.peek()==minStack.peek()){ minStack.pop(); } dataStack.pop(); } public int top() { return dataStack.peek(); } public int min() { return minStack.peek(); } }
Q:輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1、2、3、4、5是某棧的壓入順序,序列4、5、3、2、1是該壓棧序列對應的一個彈出序列,而4、3、5、1、2不可能是該壓棧序列的彈出序列。
- 技術點:棧
- 思路:借用一個輔助棧。遍歷壓棧順序,將元素放入輔助棧,然後判斷棧頂元素與彈出順序第一個元素相等,若相等則出棧並將彈出順序向後移動一位,直到不相等;最後若輔助棧不為空,說明彈出序列不是該棧的彈出順序
- 參考程式碼:
public class Solution { public boolean IsPopOrder(int [] pushA,int [] popA) { if(pushA.length == 0 || popA.length == 0) return false; Stack<Integer> s = new Stack<Integer>();//輔助棧 int popIndex = 0;//用於標識彈出序列的位置 for(int i = 0; i< pushA.length;i++){ s.push(pushA[i]); //如果棧不為空且棧頂元素等於彈出序列,則出棧並將彈出序列向後移動一位 while(!s.empty() &&s.peek() == popA[popIndex]){ s.pop(); popIndex++; } } return s.empty(); } }
Q:翻轉單詞順序列,如輸入“student. a am I”,輸出“I am a student.”
- 技術點:字串
- 思路:先翻轉整個句子,再依次翻轉每個單詞,根據空格來確定單詞的起始和終止位置
- 參考程式碼:
//方法1 public class Solution { public String ReverseSentence(String str) { char[] chars = str.toCharArray(); reverse(chars,0,chars.length - 1);//整體翻轉 int blank = -1; for(int i = 0;i < chars.length;i++){ if(chars[i] == ' '){ int nextBlank = i; reverse(chars,blank + 1,nextBlank - 1);//各個單詞翻轉 blank = nextBlank; } } reverse(chars,blank + 1,chars.length - 1);//最後一個單詞單獨進行反轉 return new String(chars); } public void reverse(char[] chars,int low,int high){ while(low < high){ char temp = chars[low]; chars[low] = chars[high]; chars[high] = temp; low++; high--; } } } //方法2:使用java提供的字串方法split public class Solution { public String ReverseSentence(String str) { if(str==null||str.trim().equals("")){ return str; } //將字串按照空格分割成字串陣列 String[] a = str.split(" "); StringBuffer o = new StringBuffer(); int i; //倒序輸出字串陣列 for (i = a.length; i >0;i--){ o.append(a[i-1]); if(i > 1){ o.append(" "); } } return o.toString(); } }
Q:請實現一個函式用來找出字元流中第一個只出現一次的字元,若沒有返回#。例如,當從字元流中只讀出前兩個字元"go"時,第一個只出現一次的字元是"g";當從該字元流中讀出前六個字元“google"時,第一個只出現一次的字元是"l"。
- 技術點:字串
- 思路:類似雜湊表,key-value對英於出現字元-出現次數
- 參考程式碼:
public class Solution { int[] hashtable=new int[256]; StringBuffer s=new StringBuffer(); public void Insert(char ch) { s.append(ch); hashtable[ch]++; } public char FirstAppearingOnce() { char[] str=s.toString().toCharArray(); for(char c:str){ if(hashtable[c]==1) return c; } return '#'; } }
Q:在一個字串(全部由字母組成)中找到第一個只出現一次的字元,並返回它的位置,如果沒有則返回 -1(需要區分大小寫)。
- 技術點: 字串
- 思路:
- 參考程式碼:
public class Solution { public int FirstNotRepeatingChar(String str) { HashMap <Character, Integer> map = new HashMap<Character, Integer>(); for(int i=0;i<str.length();i++){ if(map.containsKey(str.charAt(i))){ int time = map.get(str.charAt(i)); map.put(str.charAt(i), ++time); } else { map.put(str.charAt(i), 1); } } int pos = -1; int i=0; for(;i<str.length();i++){ char c = str.charAt(i); if (map.get(c) == 1) { return i; } } return pos; } }
Q:輸入一個字串,按字典序打印出該字串中字元的所有排列。例如輸入字串abc,則打印出由字元abc所能排列出來的所有字串abc、acb、bac、bca、cab和cba。
- 技術點:字串
-
思路:基於回溯法思想
- 參考程式碼:
public class Solution { public ArrayList<String> Permutation(String str) { List<String> resultList = new ArrayList<>(); if(str.length() == 0){ return (ArrayList)resultList; } fun(str.toCharArray(),resultList,0);//遞迴的初始值為(str陣列,空的list,初始下標0) Collections.sort(resultList); return (ArrayList)resultList; } private void fun(char[] ch,List<String> list,int i){ if(i == ch.length-1){ if(!list.contains(new String(ch))){ list.add(new String(ch)); return; } }else{ //回溯法 for(int j=i;j<ch.length;j++){ swap(ch,i,j); fun(ch,list,i+1); swap(ch,i,j); } } } private void swap(char[] str, int i, int j) { if (i != j) { char t = str[i]; str[i] = str[j]; str[j] = t; } } }
Q:在一個二維陣列中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函式,輸入這樣的一個二維陣列和一個整數,判斷陣列中是否含有該整數。
- 技術點: 陣列
- 思路:從左下角開始查詢,當比關鍵字小時右移,反之上移
- 參考程式碼:
public class Solution { public boolean Find(int target, int [][] array) { int rowCount = array.length; int colCount = array[0].length; int i,j; for(i=rowCount-1,j=0;i>=0&&j<colCount;){ if(target == array[i][j]){ return true; }else if(target < array[i][j]){ i--; continue; }else if(target > array[i][j]){ j++; continue; } } return false; } }
Q:找出陣列中有出現的次數超過陣列長度的一半的數字,如果不存在則輸出0。例如輸入一個長度為9的陣列{1,2,3,2,2,2,5,4,2},由於數字2在陣列中出現了5次,超過陣列長度的一半,因此輸出2。
- 技術點: 陣列
- 思路:在遍歷陣列時儲存兩個值,一是陣列中一個數字,一是次數;遍歷下一個數字時,若它與之前儲存的數字相同,則次數加1,否則減1;若次數為0,則儲存下一個數字,並將次數置為1;遍歷結束後,所儲存的數字即為所求;然後再判斷它是否符合條件即可。
- 參考程式碼:
public class Solution { public int MoreThanHalfNum_Solution(int [] array) { int length=array.length; if(array==null||length<=0){ return 0; } //找到最後還留住的元素,可能是目標物件 int result=array[0]; int times=1; for(int i=1;i<length;i++){ if(times==0){ result=array[i]; times=1; }else{ if(array[i]==result){ times++; }else{ times--; } } } //對可能的目標物件進行驗證 times=0; for(int i=0;i<length;i++){ if(result==array[i]){ times++; } } if(times*2<=length){ result=0; } return result; } }
Q:輸入一個正整數陣列,把數組裡所有數字拼接起來排成一個數,列印能拼接出的所有數字中最小的一個。例如輸入陣列{3,32,321},則打印出這三個數字能排成的最小數字為321323。
- 技術點: 陣列
- 思路:通過一個比較器對資料進行兩兩比較,規則為若a+b<b+a,則a排在前
- 參考程式碼:
public class Solution { public String PrintMinNumber(int [] numbers) { String s=""; ArrayList<Integer> list= new ArrayList<Integer>(); for(int i=0;i<numbers.length;i++){ list.add(numbers[i]); } Collections.sort(list, new Comparator<Integer>(){ public int compare(Integer str1,Integer str2){ String s1=str1+""+str2; String s2=str2+""+str1; return s1.compareTo(s2);//如果s1>s2,則str1和str2交換,反之不變 } }); for(int j:list){ s+=j; } return s; } }
Q:輸入一個遞增排序的陣列和一個數字S,在陣列中查詢兩個數,使得他們的和正好是S,如果有多對數字的和等於S,輸出兩個數的乘積最小的。
- 技術點:陣列
- 思路:用兩個指標一頭一尾;若加和比S大,則尾指標下移一個,若比S小,則頭指標上移一個;最先找到的兩個數,差最大乘積最小
- 參考程式碼:
public class Solution { public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) { ArrayList<Integer> list = new ArrayList<Integer>(); if (array == null || array.length < 2) { return list; } int i=0,j=array.length-1; while(i<j){ if(array[i]+array[j]==sum){ list.add(array[i]); list.add(array[j]); return list; }else if(array[i]+array[j]>sum){ j--; }else{ i++; } } return list; } }
Q:輸入一個整數陣列,實現一個函式來調整該陣列中數字的順序,使得所有的奇數位於陣列的前半部分、所有的偶數位於陣列的後半部分,並保證奇數和奇數、偶數和偶數之間的相對位置不變。
- 技術點:程式碼的完整性
- 思路:
- 用排列:插排思想,如果當前數是奇數,就一直往前找,遇到偶數就往它前面插
- 空間換時間:先統計奇數的個數;再新建一個等長陣列,設定兩個指標,奇數指標從0開始,偶數指標從奇數個數的末尾開始
- 參考程式碼:
//方法1:時間複雜度O(n2)、空間複雜度O(1) public class Solution { public void reOrderArray(int [] array) { if(array.length==0||array.length==1) return; for(int i=1;i<array.length;i++){ int temp = array[i]; if(array[i] % 2 == 1){ int j = i; while(j >= 1 && array[j-1] % 2 == 0){ array[j] = array[j-1]; j--; } array[j] = temp; } } } } //方法2:時間複雜度O(n)、空間複雜度O(n) public class Solution { public void reOrderArray(int [] array) { if(array.length==0||array.length==1) return; int oddCount=0,oddBegin=0; int[] newArray=new int[array.length]; for(int i=0;i<array.length;i++){ if((array[i]&1)==1) oddCount++;//統計奇數個數 } for(int i=0;i<array.length;i++){ if(array[i]%2==1){ newArray[oddBegin++]=array[i];//奇數項從0開始 }else{ newArray[oddCount++]=array[i];//偶數項從奇數個數的末尾開始 } } for(int i=0;i<array.length;i++){ array[i]=newArray[i]; } } }
Q:實現從上到下按層列印二叉樹,同一層結點從左至右輸出,每一層輸出一行。
- 技術點: 樹
- 思路:BFS
- 參考程式碼:
//節點類TreeNode public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this.val = val; } public class Solution { ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) { ArrayList<ArrayList<Integer>> alist = new ArrayList<ArrayList<Integer>>();// 結果集 if (pRoot == null) return alist; Queue<TreeNode> queue = new LinkedList<TreeNode>();// 佇列 queue.add(pRoot); while (!queue.isEmpty()) { ArrayList<Integer> list = new ArrayList<Integer>();// 每層結果 int count=queue.size();//每層節點個數 for (int i = 0; i < count; i++) { TreeNode node = queue.peek(); list.add(node.val); if (node.left != null)queue.add(node.left); if (node.right != null)queue.add(node.right); queue.poll(); } alist.add(list); } return alist; } }
Q:給定一棵二叉搜尋樹,請找出其中的第k小的結點。例如(5,3,7,2,4,6,8)中第三小結點的值為4。
- 技術點: 樹
- 思路:二叉搜尋樹的中序遍歷結果正好是從小到大排序好的,按照中序遍歷順序找第k個節點
- 參考程式碼:
//節點類TreeNode同上 public class Solution { int index = 0; //計數器 TreeNode KthNode(TreeNode root, int k){ if(root != null){ TreeNode node = KthNode(root.left,k); if(node != null) return node;//注意需要判斷是否為空,否則如果找到符合要求的節點只能返回到上一層,而不能返回到頂層,使得輸出結果為null index ++; if(index == k) return root; node = KthNode(root.right,k); if(node != null) return node;//理由同上 } return null; } }
Q:輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。
- 技術點:樹
- 思路:從下往上遍歷,如果子樹是平衡二叉樹,則返回子樹的高度,反之直接停止遍歷,這樣至多隻對每個結點訪問一次
- 參考程式碼:
//節點類TreeNode同上 public class Solution { public boolean IsBalanced_Solution(TreeNode root) { return getDepth(root) != -1; } private int getDepth(TreeNode root) { if (root == null) return 0; int left = getDepth(root.left); if (left == -1) return -1; int right = getDepth(root.right); if (right == -1) return -1; return Math.abs(left - right) > 1 ? -1 : 1 + Math.max(left, right); } }
Q:一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。
- 技術點: 遞迴和迴圈
- 思路:對於第n個臺階來說,只能從n-1或n-2的臺階跳上來,因此跳到n階的跳法=跳到n-1階的方案+跳到n-2階的方案,即F(n) = F(n-1) + F(n-2),是個斐波拉契數序列
- 參考程式碼:
//遞迴法 public class Solution { public int JumpFloor(int target) { if (target <= 2) { return target; } else { returnJumpFloor(target-1)+JumpFloor(target-2); } } } //迭代法 public class Solution { public int JumpFloor(int target) { if (target <= 2) { return target; } int f1=1; int f2=2; int f=0; for(int i=3;i<=target;i++){ f=f1+f2; f1=f2; f2=f; } return f; } }
Q:我們可以用2*1的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個2*1的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?
- 技術點: 遞迴和迴圈
-
思路:2*n的覆蓋方案=2*(n-1)的覆蓋方案+2*(n-2)的覆蓋方案,即F(n) = F(n-1) + F(n-2),是個斐波拉契數序列
- 參考程式碼:程式碼同上
Q:地上有一個m行和n列的方格,一個機器人從座標(0,0)的格子開始移動,每一次只能向左、右、上、下四個方向移動一格,但是不能進入行座標和列座標的數位之和大於k的格子。 例如當k為18時,機器人能夠進入方格(35,37),因為3+5+3+7 = 18;但是不能進入方格(35,38),因為3+5+3+8 = 19。請問該機器人能夠達到多少個格子?
- 技術點: 知識遷移能力
- 思路:DFS
- 參考程式碼:
public class Solution { public int movingCount(int threshold, int rows, int cols){ if(rows <= 0 || cols <= 0 || threshold < 0) return 0; boolean[] visited = new boolean[rows * cols]; return dfs(threshold,rows,cols,visited,0,0); } privateint dfs(int threshold, int rows, int cols, boolean[] visited, int x, int y) { //滿足終止條件的格子:不屬於方格、已經標記過、位數和大於閾值 if(x < 0 || x >= cols || y < 0 || y >= rows || getDigitSum(x) + getDigitSum(y) > threshold || visited[x + y * cols]) return 0; visited[x + y * cols] = true; return 1 + dfs(threshold, rows, cols, visited, x, y - 1) + dfs(threshold, rows, cols, visited, x + 1, y) + dfs(threshold, rows, cols, visited, x, y + 1) + dfs(threshold, rows, cols, visited, x - 1, y); } //位數和 private int getDigitSum(int i) { int sum = 0; while(i > 0) { sum += i % 10; i /= 10; } return sum; } }
Q:求出從1 到 n 中1出現的次數。
- 技術點: 時間效率
- 思路: 從1到n整數中1出現的次數:O(logn)演算法
Q:把只包含質因子2、3和5的數稱作醜數。例如6、8都是醜數,而14不是,因為它包含質因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。
- 技術點: 時間空間效率的平衡
- 思路:對於第i個數,它一定是之前已存在數的2倍、3倍或5倍
- 參考程式碼:
public class Solution { public int GetUglyNumber_Solution(int index) { if (index < 7) return index; int[] res=new int[index]; res[0] = 1; int t2 = 0, t3 = 0, t5 = 0; for (int i = 1; i < index; ++i){ res[i] = Math.min(res[t2] * 2, Math.min(res[t3] * 3, res[t5] * 5)); if (res[i] == res[t2] * 2) t2++; if (res[i] == res[t3] * 3) t3++; if (res[i] == res[t5] * 5) t5++; } return res[index - 1]; } }
Q:輸入一個矩陣,按照從外向裡以順時針的順序依次打印出每一個數字。例如如果輸入如下4 X 4矩陣, 則依次打印出數字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
- 技術點: 畫圖讓抽象形象化
- 思路:用左上和右下的座標定位出一次要旋轉列印的資料,一次旋轉列印結束後,往對角分別前進和後退一個單位。注意單行或者單列的情況。
- 參考程式碼:
public class Solution { public ArrayList<Integer> printMatrix(int [][] matrix) { int row = matrix.length; int col = matrix[0].length; ArrayList<Integer> res = new ArrayList<>(); if (row == 0 || col == 0){ return res; } int left = 0, top = 0, right = col - 1, bottom = row - 1; // 定義四個關鍵變數,表示左上和右下的列印範圍 while (left <= right && top <= bottom) { for (int i = left; i <= right; ++i){ res.add(matrix[top][i]);//從左到右 } for (int i = top + 1; i <= bottom; ++i){ res.add(matrix[i][right]);//從上到下 } if (top != bottom) for (int i = right - 1; i >= left; --i){ res.add(matrix[bottom][i]);//從右到左 } if (left != right) for (int i = bottom - 1; i > top; --i){ res.add(matrix[i][left]);//從下到上 } left++;top++;right--;bottom--;//縮小一圈,重新定位列印範圍 } return res; } }
(持續更新...)