1. 程式人生 > >8 數據結構與算法

8 數據結構與算法

重復數 子數組和 解決 介紹 適用於 尾結點 字符數組 應該 暴力法

8.1 鏈表 8.1.1 如何實現單鏈表的增刪操作
  • 技術分享圖片

8.1.2 如何從鏈表中刪除重復的數據
  • 如何從鏈表中刪除重復數據,最容易想到的方法就是遍歷鏈表,把遍歷到的值存儲到一個Hashtable中,在遍歷過程中,若當前訪問的值在Hashtable中已經存在,則說明這個數據是重復的,因此就可以刪除。
  • 主要思路為對鏈表進行雙重循環遍歷,外循環正常遍歷鏈表,假設外循環當前遍歷的結點為cur,內循環從cur開始遍歷,若碰到與cur所指向結點值相同,則刪除這個重復結點。
8.1.3 如何找出單鏈表中的倒數第K個元素
  • 兩個指針 先行指針比後行指針先行k-1個元素
8.14 如何實現鏈表的反轉
  • 具體的反轉過程,例如,i, m, n是3個相鄰的結點,假設經過若千步操作,已經把結點i之前的指針調整完畢,這些結點的next指針都指向前面-一個結點。現在遍歷到結點m,當然,需要調整結點的next指針,讓它指向結點i,但需要註意的是,一旦調整了指針的指向,鏈表就斷開了,因為已經沒有指針指向結點n,沒有辦法再遍歷到結點n了,所以為了避免鏈表斷開,需要在調整m的next之前要把n保存下來。接下來試著找到反轉後鏈表的頭結點,不難分析出反轉後鏈表的頭結點是原始鏈表的尾結點,即next;為空指針的結點。
  • 下面給出非遞歸方法實現鏈表的反轉的實現代碼。
    • 技術分享圖片

    • 技術分享圖片

8.1.5 如何從尾到頭輸出單鏈表
  • 從頭到尾輸出單鏈表比較簡單,通過借鑒的想法,要想解決本問題,很自然地想把鏈表中鏈接結點的指針反轉過來,改變鏈表的方向,然後就可以從尾到頭輸出了,但該方法需要額外的操作,是否還有更好的方法呢?答案是有。
  • 接下來的想法是從頭到尾遍歷鏈表,每經過-一個結點,把該結點放到一個棧中。當遍歷完整個鏈表後,再從棧頂開始輸出結點的值,此時輸出的結點的順序已經反轉過來了。該方法雖然沒有只需要遍歷一遍鏈表,但是需要維護--個額外的棧空間,實現起來會比較麻煩。
  • 是否還能有更高效的方法?既然想到了棧來實現這個函數,而遞歸本質上就是一一個棧結構,於是很自然地又想到了遞歸來實現。要實現反過來輸出鏈表,每訪問到一個結點,先遞歸輸出它後面的結點,再輸出該結點自身,這樣鏈表的輸出結果就反過來了。
  • 具體實現代碼如下:
    • public Node SearchMid( Node head){
    • Node p = this.head;
    • Node q = this.head;
    • while(p != null && p.next != null && p.next.next != null) {
    • p = p.next.next;
    • q=q.next;
    • }
    • return q;
    • }
8.1.7 如何檢測一個鏈表是否有環
  • 定義兩個指針fast與slow,其中,fast 是快指針,slow 是慢指針,二者的初始值都指向鏈表頭,slow 每次前進一步,fast 每次前進兩步,兩個指針同時向前移動,快指針每移動一次都要跟慢指針比較,直到當快指針等於慢指針為止,就證明這個鏈表是帶環的單向鏈表,否則,證明這個鏈表是不帶環的循環鏈表( fast先行到達尾部為NULL,則為無環鏈表)。
  • 具體實現代碼如下:
    • public boolean IsLoop( Node head) {
    • Node fast = head;
    • Node slow = head;
    • if( fast == null) {
    • return false;
    • }
    • while(fast != null && fast.next != null) {
    • fast = fast.next.next;
    • slow = slow.next;
    • if( fast == slow) {
    • return true;
    • }
    • return !( fast == null || fast.next == null) ;
    • }
  • 上述方法只能用來判斷鏈表是否有環,那麽如何找到環的人口點呢?如果單鏈表有環,按照上述思路,當走得快的指針fast與走得慢的指針slow相遇時,slow指針肯定沒有遍歷完鏈表,而fast指針已經在環內循環了n圈(n>=1)。假設slow 指針走了s步,則fast指針走了2s步(fast 步數還等於s加上在環上多轉的n圈),設環長為r,則滿足如下關系表達式:
    • 2s=s + nr
    • s=nr
  • 設整個鏈表長L,人口環與相遇點距離為x,起點到環人口點的距離為a,則滿足如下關系表達式:
    • a+x= nr
    • a+x=(n-1)r+r=(n-1)r+L-a
    • a=(n-1)r+(L-a-x)
  • (L-a-x)為相遇點到環人口點的距離,從鏈表頭到環人口點等於(n- 1)循環內環 + 相遇點到環入口點,於是在鏈表頭與相遇點分別設一個指針,每次各走一步,兩個指針必定相遇,且相遇第一點為環入口點。
  • 具體實現代碼如下:
    • public Node FindLoopPort( Node head){
    • Node slow = head , fast = head ;
    • while( fast != null && fast.next != null){
    • slow = slow.next;
    • fast = fast.next.next;
    • if( slow == fast) break ;
    • }
    • if( fast == null || fast. next == null) return null;
    • slow = head;
    • while( slow != fast) {
    • slow = slow.next;
    • fast = fast.next;
    • }
    • return slow;
    • }
8.1.8 如何在不知道頭指針的情況下刪除指定節點
  • 可以分為兩種情況來討論:
    • ①若待刪除的結點為鏈表尾結點,則無法刪除,因為刪除後無法使其前驅結點的next指針置為空;
    • ②若待刪除的結點不是尾結點,則可以通過交換這個結點與其後繼結點的值,然後刪除後繼結點。
    • 具體實現代碼如下:
      • public boolean deleteNode( Node n) {
      • if( n == null || n.next == null)
      • return false;
      • int tmp = n.data;
      • n.data = n.next.data;
      • n.next.data = tmp;
      • n.next = n.next.next;
      • return true;
      • }
8.1.9 如何判斷兩個鏈表是否相交
  • 如果兩個鏈表相交,那麽它們一定有著相同的尾結點,因此實現思路為:分別遍歷兩個鏈表,記錄它們的尾結點,如果它們的尾結點相同,那麽這兩個鏈表相交,否則不相交。
  • 具體實現代碼
    • public boolean isIntersect( Node h1, Node h2){
    • if(h1 ==null || h2 == null)
    • return false;
    • Node taill = h1 ;
    • //找到鏈表h1的最後一個結點
    • while( taill.next ! = null)
    • taill = taill. next;
    • Node tail2 = h2 ;
    • //找到鏈表h2的最後一個結點
    • while( tail2.next ! = null){
    • tail2 = tail2.next ;
    • }
    • return taill == tail2;
    • }
  • 由於這個算法只需要分別遍歷一-次兩個鏈表,因此算法的時間復雜度為O(len1 + len2),其中len1和len2分別代表兩個鏈表的長度。
  • 引申:
    • 如果兩個鏈表相交,如何找到它們相交的第一個結點?
    • 首先分別計算兩個鏈表head1、head2 的長度len1和len2 ( 假設len1 > len2),接著先對鏈表head1遍歷(lenl - len2)個結點到結點p,此時結點p與head2到它們相交的結點的距離相同,此時同時遍歷兩個鏈表,直到遇到相同的結點為止,這個結點就是它們相交的結點。需要註意的是,在查找相交的第一一個結點前,需要先判斷兩個鏈表是否相交,只有在相交的情況下才有必要去找它們的交點,否則根本就沒有必要了。
    • 程序代碼如下:
      • public static Node getFirstMeetNode( Node h1, Node h2) {
      • if(h1 == null || h2 == null )
      • return null ;
      • Node tail1 = h1;
      • int len1 =1;
      • //找到鏈表h1的最後-一個結點
      • while( taill.next != null){
      • taill = taill.next;
      • lenl++;
      • Node tail2 = h2;
      • int len2=1;
      • //找到鏈表h2的最後一個結點
      • while( tail2.next != null){
      • tail2 = tail2. next ;
      • len2 ++ ;
      • }
      • //兩鏈表不相交
      • if(taill ! = tail2) {
      • return null;
      • }
      • Node t1 =h1 ;
      • Node 12 = h2;
      • //找出較長的鏈表,先遍歷
      • if(len1 > len2) {
      • int d=lenl一len2 ;
      • while(d! =0) {
      • tl = t1.next;
      • d-- ;
      • }
      • }
      • else{
      • int d= len2 - lenl;
      • while(d! =0){
      • t2 = t2.next;
      • d--;
      • }
      • }
      • while(t1! =t2) {
      • t1 =t1. next;
      • t2 = t2. next;
      • return tl ;
      • }
      • }
  • 同理,由於這個算法也只需要分別遍歷一-次兩個鏈表,因此算法的時間復雜度為O(len1 +len2),其中len1和len2分別代表兩個鏈表的長度。當然,在具體實現時可以使用前面已經實現的方法來判斷兩個鏈表是否相交,也可以利用前面已經實現的方法來分別計算兩個鏈表的長度。但這種方法也存在著-一個缺點:需要對每個鏈表遍歷兩遍。第- ~遍用來判斷鏈表是否相交,第二遍計算鏈表的長度,因此效率會比上例中的實現方式低。其優點是代碼簡潔,可用性強。
8.2 棧與隊列

Stack(棧)和 Queue(隊列)

8.2.1 棧與隊列有哪些區別
    • LIFO(Last In First Out,後進先出)
  • 隊列
    • FIFO(First In Frist Out,先進先出)
  • 技術分享圖片

8.2.2 如何實現棧
  • 可以采用數組與鏈表這兩種方法來實現棧。
  • 下面給出用數組實現棧的代碼:
技術分享圖片


  • 下面給出采用鏈表實現棧的代碼:
    • 技術分享圖片

  • 需要註意的是,上述的實現不是線程安全的。若要實現線程安全的棧,則需要對人棧和出棧等操作進行同步(synchronized)。
8.2.3 如何用0(1)的時間復雜度求棧中最小元素
  • 由於棧具有後進先出的特點,因此push和pop只需要對棧頂元素進行操作。如果使用上述的實現方式,只能訪問到棧頂的元素,而無法得到棧中最小的元素。當然,可以用另外一個變量來記錄棧底的位置,通過遍歷棧中的所有元素找出最小值,但是這種方法的時間復雜度為O(n),那麽如何才能用0(1)的時間復雜度求出棧中最小的元素呢?在算法設計中,經常會采用空間來換取時間的方式來提高時間復雜度,也就是說,采用額外的存儲空間來降低操作的時間復雜度。具體來講就是在實現時使用兩個棧結構,一個棧用來存儲數據,另一個棧用來存儲棧的最小元素。其實現思路如下:如果當前人棧的元素比原來棧中的最小值還小,則把這個值壓人保存最小元素的棧中;在出棧時,如果當前出棧的元素恰好為當前棧中的最小值,保存最小值的棧頂元素也出棧,使得當前最小值變為其入棧之前的那個最小值。為了簡單起見,可以在棧中保存Interger類型,采用前面用鏈表方式實現的棧,
  • 實現代碼如下:
技術分享圖片 技術分享圖片

8.2.4如何實現隊列
  • 與棧類似,隊列也可以采用數組和鏈表兩種方式來實現。
  • 下面給出采用鏈表方式實現隊列的代碼:
技術分享圖片 技術分享圖片


  • 下面介紹數組實現隊列的方式,為了實現多線程安全,增加了對隊列操作的同步,實現代碼如下:
技術分享圖片

技術分享圖片

8.2.5 如何用兩個棧模擬隊列操作
  • 假設使用棧A與棧B模擬隊列Q,A為插人棧,B為彈出棧,以實現隊列Q。再假設A和B都為空,可以認為棧A提供入隊列的功能,棧B提供出隊列的功能。要人隊列,入棧A即可,而出隊列則需要分兩種情況考慮
    • 1)若棧B不為空,則直接彈出棧B的數據。
    • 2)若棧B為空,則依次彈出棧A的數據,放人棧B中,再彈出棧B的數據。
  • 以上情況可以利用前面介紹的棧來實現,也可以采用Java類庫提供的Stack來實現,下面代碼是采用Java內置的Stack來實現的:
技術分享圖片


  • 引申:如何使用兩個隊列實現棧?
    • 假設使用隊列q1與隊列q2模擬棧S, q1為人隊列,q2為出隊列。
    • 實現思路:
      • 可以認為隊列q1提供壓棧的功能,隊列q2提供彈棧的功能。要壓棧,人隊列q1即可,而當彈棧時,出隊列則需要分兩種情況考慮:
        • 1)若隊列q1中只有一一個元素,則讓q1中的元素出隊列並輸出即可。
        • 2)若隊列q1中不只一個元素,則隊列q1中的所有元素出隊列,入隊列q2,最後一個元素不人隊列B,輸出該元素,然後將隊列q2所有元素人隊列q1。
8.3 排序 排序問題一直是計算機技術研究的重要問題,排序算法的好壞直接影響程序的執行速度和輔助存儲空間的占有量,所以,各大IT企業在筆試面試中也經常出現有關排序的題目,本節將詳細分析常見的各種排序算法,並從時間復雜度、空間復雜度、適用情況等多個方面對它們進行綜合比較。 8.3.1如何進選擇排序
  • 選擇排序是-一種簡單直觀的排序算法,其基本原理如下:對於給定的一組記錄,經過第一輪比較後得到最小的記錄,然後將該記錄與第一個記錄的位置進行交換;接著對不包括第一個記錄以外的其他記錄進行第二輪比較,得到最小的記錄並與第-二個記錄進行位置交換;重復該過程,直到進行比較的記錄只有一個時為止。以數組{ 38,65,97, 76,13, 27,49 }為例,
  • 選擇排序的具體步驟如下:
技術分享圖片

8.3.2如何進行插人排序
  • 對於給定的一組記錄,初始時假設第一個記錄自成一個有序序列,其余記錄為無序序列。接著從第二個記錄開始,按照記錄的大小依次將當前處理的記錄插人到其之前的有序序列中,直至最後一個記錄插人到有序序列中為止。仍以數組{ 38,65,97 , 76,13, 27, 49 }為例,
  • 直接插入排序的具體步驟如下。
技術分享圖片


8.3.4如何進歸並排序
  • 歸並排序是利用遞歸與分治技術將數據序列劃分成為越來越小的半子表,再對半子表排序,最後再用遞歸方法將排好序的半子表合並成為越來越大的有序序列。歸並排序中,“歸”代表的是遞歸的意思,即遞歸的將數組折半的分離為單個數組,例如數組: [2,6,1,0],會先折半,分為[2,6]和[1,0]兩個子數組, 然後再折半將數組分離,分為[2]、[6]和[1]、[0]。“並”就是將分開的數據按照從小到大或者從大到小的順序在放到-一個數組中。如上面的[2]、[6]合並到一個數組中是[2,6],[1]、[0]合並到一個數組中是[0,1],然後再將[2,6]和[0,1]合並到一個數組中即為[0,1,2,6]。
  • 歸並排序算法的原理如下:對於給定的一-組記錄(假設共有n個記錄),首先將每兩個相鄰的長度為1的子序列進行歸並,得到n/2 (向上取整)個長度為2或1的有序子序列,再將其兩兩歸並,反復執行此過程,直到得到一個有序序列。
  • 所以,歸並排序的關鍵就是兩步:第一步,劃分半子表;第二步,合並半子表。以數組{49 ,38 ,65 ,97 ,76,13 ,27}為例,歸並排序的具體步驟如下:
  • 技術分享圖片
  • 二路歸並排序的過程需要進行logn趟。每一趟歸並排序的操作,就是將兩個有序子序列進行歸並,而每一-對有序子序列歸並時,記錄的比較次數均小於等於記錄的移動次數,記錄移動的次數均等於文件中記錄的個數n,即每一趟歸並的時間復雜度為O(n)。因此,二路歸並排序的時間復雜度為0( nlogn)。
8.3.5 如何進行快速排序
  • 快速排序,顧名思義,是一種速度快,效率高的排序算法。
  • 快排原理:在要排的數(比如數組A)中選擇一個中心值key(比如A[0]),通過一趟排序將數組A分成兩部分,其中以key為中心,key右邊都比key大,key左邊的都key小,然後對這兩部分分別重復這個過程,直到整個有序。
整個快排的過程就簡化為了一趟排序的過程,然後遞歸調用就行了。 一趟排序的方法:
  • 1,定義i=0,j=A.lenght-1,i為第一個數的下標,j為最後一個數下標
  • 2,從數組的最後一個數Aj從右往左找,找到第一小於key的數,記為Aj;
  • 3,從數組的第一個數Ai 從左往右找,找到第一個大於key的數,記為Ai;
  • 4,交換Ai 和Aj
  • 5,重復這個過程,直到 i=j
  • 6,調整key的位置,把A[i] 和key交換
技術分享圖片


  • java代碼
    • public class testMain {
    • public static void sort(int array[], int low, int high) {
    • int i, j;
    • int index;
    • if (low >= high)
    • return;
    • i = low;
    • j = high;
    • index = array[i];
    • //index = array[(low+high)/2];
    • //array[(low+high)/2] = array[i];
    • while (i < j) {
    • while (i < j && array[j] >= index) {
    • j--;
    • }
    • if (i < j) {
    • array[i++] = array[j];
    • }
    • while (i < j && array[i] < index) {
    • i++;
    • }
    • if (i < j) {
    • array[j--] = array[i];
    • }
    • array[i] = index;
    • sort(array, low, i - 1);
    • sort(array, i + 1, high);
    • }
    • }
    • public static void quickSort(int array[]) {
    • sort(array, 0, array.length - 1);
    • }
    • public static void main(String[] args) {
    • int i = 0;
    • int a[] = { 5, 4, 9, 8, 7, 6, 0, 1, 3, 2 };
    • quickSort(a);
    • for (i = 0; i < a.length; i++) {
    • System.out.println(a[i]);
    • }
    • }
    • }
8.3.6 如何進行希爾排序 希爾排序也被稱為“ 縮小增量排序”,其基本原理如下:先將待排序的數組元素分成多個子序列,使得每個子序列的元素個數相對較少,然後對各個子序列分別進行直接插人排序,待整個待排序序列“ 基本有序後”,最後再對所有元素進行一次直接插入排序。 8.3.7 如何進行堆排序 堆是一種特殊的樹形數據結構,其每個結點都有一一個值,通常提到的堆都是指一棵完全二叉樹,根結點的值小於(或大於)兩個子結點的值,同時,根結點的兩個子樹也分別是一個堆。 堆排序是一樹形選擇排序,在排序過程中,將R[1...n]看作一顆完全二叉樹的順序存儲結構,利用完全二叉樹中父結點和子結點之間的內在關系來選擇最小的元素。 8.3.8 各種排序算法有什麽差異
  • 不穩定:
    • 選擇排序(selection sort)— O(n2)
    • 快速排序(quicksort)— O(nlogn) 平均時間, O(n2) 最壞情況; 對於大的、亂序串列一般認為是最快的已知排序
    • 堆排序 (heapsort)— O(nlogn)
    • 希爾排序 (shell sort)— O(nlogn)
    • 基數排序(radix sort)— O(n·k); 需要 O(n) 額外存儲空間 (K為特征個數)
  • 穩定:
    • 插入排序(insertion sort)— O(n2)
    • 冒泡排序(bubble sort) — O(n2)
    • 歸並排序 (merge sort)— O(n log n); 需要 O(n) 額外存儲空間
    • 二叉樹排序(Binary tree sort) — O(nlogn); 需要 O(n) 額外存儲空間
    • 計數排序 (counting sort) — O(n+k); 需要 O(n+k) 額外存儲空間,k為序列中Max-Min+1
    • 桶排序 (bucket sort)— O(n); 需要 O(k) 額外存儲空間
8.4 位運算 8.4.1 如何用位操作實現乘法運算
  • 把數字向左移動n位相當於把改數乘以2的n次方。
8.4.2 如何判斷一個數是否為2的n次方
  • 技術分享圖片

  • 上述算法的時間復雜度為0( logn)。 那麽是否存在效率更高的算法呢?通過對2^0,2^1,2^2,.,2^n 進行分析,發現這些數字的二進制形式分別為: 1,10,100,….從二進制的表示可以看出,如果-一個數是2 的n次方,那麽這個數對應的二進制表示中只有一位是1,其余位都為0。因此,判斷-一個數是否為2的n次方可以轉換為這個數對應的二進制表示中是否只有一位為1。如果一個數的二進制表示只有一位是1,例如num =00010000,那麽num-1的二進制表示為num-1=00001111,由於num與num-1二進制表示中每一位都不相同,因此num&( num- 1)的運算結果為0,可以利用這種方法來判斷一一個數是否為2的n次方。
    • 具體實現代碼如下:
    • 技術分享圖片

8.4.3 如何求二進制數中1的個數
  • 問題描述:給定-一個整數,輸出這個整數二進制表示中1的個數,例如,給定整數7,其二進制表示為111,因此輸出結果為3。該問題可以采用位操作來完成。具體思路如下:首先,判斷這個數的最後- -位是否為1,如果為1,則計數器加1,然後,通過右移丟棄掉最後一位。循環執行該操作直到這個數等於0為止。在判斷二進制表示的最後一一位是否為1時,可以采用與運算來達到這個目的。
    • 具體實現代碼如下:
    • 技術分享圖片

  • 給定一個數n,每進行一次n&(n-1)計算,起結果中都會少了一位1,而且是最後一位。利用這個特性編寫如下代碼。
    • 技術分享圖片

8.5 數組 8.5.1 如何尋找數組中的最小值和最大值
  • 1. 問題分解法。 將問題分解,找最大、找最小。
  • 2. 取單元素法。一次找最大最小,及取出一個值比較是否是最大,是否是最小。
  • 3. 取雙元素法。 每次取兩個元素,大的與max比較,小的與min比較。
  • 4. 數組元素位移法。 將數組中相鄰的兩個數大的放左邊小的放右邊。對大值掃描一次取最大值。對小值掃描一次取最小值。
  • 5. 分治法。 將數組分為兩組,分別取最大值、最小值。則兩組值中取最大值、最小值。
8.5.2 如何找到數組中第二大的數
  • 1. 可先排序然後找到第二大的數。(一般為快速排序算法)
  • 2. 設置兩值,max 、sec_max
    • 技術分享圖片

8.5.3 如何求最大子數組之和
  • 問題描述: -一個有n個元素的數組,這n個元素可以是正數也可以是負數,數組中連續的一個或多個元素可以組成一個連續的子數組,一一個數組可能有多個這種連續的子組,求子數組和的最大值,例如:對於數組{1, -2, 4, 8, -4, 7, -1, -5}而言,其最大和的子數組為{4, 8,-4, 7},最大值為15。
  • 方法一:暴力法
  • 最簡單也是最容易想到的方法就是找出所有子數組,然後求出子數組的和,在所有子數組的和中取最大值。
技術分享圖片 技術分享圖片


  • 方法二:重復利用已經計算的子數組和。
  • 例如Sum[i,j] = Sum[i,j-1] +arr[j],采用這種方法可以省去計算Sum[i,j-1]的時間,因此可以提高程序的效率。示例如下:
  • 技術分享圖片

  • 方法三:動態規劃方法
  • 可以采用動態規劃的方法來降低算法的時間復雜度,實現思路如下。首先可以根據數組的最後一個元素arr[n-1]與最大字數組的關系分為以下3種情況:
    • 1)最大子數組的包含arr[n-1],即以arr[n-1]結尾。
    • 2) arr[n-1] 單獨構成最大子數組。
    • 3)最大子數組不包含arr[n-1],那麽求arr[1,*,n-1]的最大子數組可以轉換為求arr[1,.*,n-2]的最大子數組。
  • 通過上述分析可以得出如下結論:假設已經計算出( arr[0],",arr[i-1])最大的一段數,組和為All[i-1],同時也計算出(arr[0],.. ,arr[i-1])中包含arr[i-1]的最大的一-段數組和為End[i-1], 則可以得出如下關系: All[i-1] = max{arr[i-1], End[i-1],All[i-2]}。
  • 利用這個公式和動態規劃的思想可以得到如下代碼:
    • 技術分享圖片

  • 方法四:優化的動態規劃方法
  • 方法三中每次只用到End[i-1]與All[i-1],而不是整個數組中的值,因此可以定義兩個變量來保存End[i-1]與All[i-1]的值,並且可以反復利用,這樣就可以在保證時間復雜度為O( n)的同時降低空間復雜度。示例如下:
技術分享圖片 技術分享圖片

  • 在知道子數組的最大和之後,如何才能確定最大子數組的位置呢?為了得到最大子數組的位置,首先介紹另外一+種計算最大子數組和的方法。在方法三中,通過對公式End[i]= max(End[i-1] +arr[i],arr[i])的分析可以看出,當End[i-1]<0時,End[i門] = array[i],其中,End[i]表示包含array[i] 的子數組和,如果某-一個值使得End[i-1] <0,那麽就從arr[ i門]重新開始。示例如下:
  • 技術分享圖片

8.5.4 如何找出數組中重復元素最多的數
  • 問題描述:對於數組{1,1,2,2,4,4,4,4,5,5,6,6,6}, 元素1出現的次數為2次,元素2出現的次數為2次,元素4出現的次數為4次,元素5出現的次數為2次,元素6出現的次數為3次,找出數組中出現重復次數最多的數。
  • 上述問題中,程序的輸出應該為元素4。可以采取如下兩種方法來計算數組中重復次數最多的數。
  • 方法一:空間換時間。可以定義一個數組int count[ MAX],並將其數組元素都初始化為0,然後執行for(int i=0;i<100;i ++ )count[A[i]] ++操作,在count數組中找最大的數,即為重復次數最多的數。這是一種典型的空間換時間的算法。一般情況下,除非內存空間足夠大,一般不采用這種方法。
  • 方法二:使用Map映射表。通過引入Map映射表( Map提供- -對一的數據處理能力,其中第-一個為關鍵字,每個關鍵字只能在Map中出現-一次,第二個稱為該關鍵字的值)來記錄每一個元素出現的次數,然後判斷次數大小,進而找出重復次數最多的元素。示例如下:
技術分享圖片


8.5.5 如何求數組中兩兩相加等於20的組合種數
  • 問題描述:給定一一個數組{1,7,17,2,6,3,14},這個數組中滿足條件的有兩對組合 17 + 3 = 20和6 + 14 =20。
  • 方法一:“蠻力”法
  • 最容易想到的方法就是采用兩重循環遍歷數組來判斷兩個數的和是否為20。
  • 實現代碼如下:
  • 技術分享圖片

  • 由於采用了雙重循環,因此這個算法的時間復雜度為O(n^2)。
  • 方法二:排序法
  • 先對數組元素進行排序,可以選用堆排序或快速排序,此時算法的時間復雜度為0( nlogn),然後對排序後的數組分別從前到後和從後到前遍歷,假設從前往後遍歷的下標為begin,從後往前遍歷的下標為end,那麽當滿足arr[ begin] + arr[ end] <20時,如果存在兩個數的和為20,那麽這兩個數一-定在[ begin +1,end]之間;當滿足ar[ begin] + arr[ end] > 20時,如果存在兩個數的和為20,那麽這兩個數一定在[ begin ,end +1]之間。這個過程的時間復雜度為0(n),因此整個算法的時間復雜度為0( nlogn)。
  • 實現代碼如下:
  • 技術分享圖片

8.5.6 如何把個數組循環右移k位
  • 假設要把數組序列12345678 右移2位變為78123456,比較移位前後數組序列的形式,不難看出,其中有兩段序列的順序是不變的,即78和123456,可以把這兩段看作兩個整體,右移k位就是把數組的兩部分交換一下。鑒於此,可以設計這樣一種算法,步驟如下,(以數組序列12345678為例) :
    • 1)逆序數組子序列123456, 數組序列的形式變為65432178。
    • 2)逆序數組子序列78,數組序列的形式變為65432187。
    • 3)全部逆序,數組序列的形式變為78123456。
  • 程序代碼如下:
技術分享圖片


8.5.7 如何找出數組中第k個最小的數
  • 問題描述:給定一個無序的數組,從一個數組中找出第k個最小的數,例如,對於給定數組序列{1,5,2,6,8,0,6},其中第4小的數為5。
  • 方法一:排序法
  • 最容易想到的方法就是對數組進行排序,排序後的數組中第k-1個位置上的數字即為數組的第k個最小的數(原因是數組下標從0開始計數),這種方法最好的時間復雜度為O( nlogn)。
  • 方法二:“剪枝”法
  • 采用快速排序的思想來實現。主要思路如下:選一個數tmp=a[n-1]作為樞紐,把比它小的數都放在它的左邊,比它大的數都放在它的右邊,然後判斷tmp的位置,如果它的位置為k-1,那麽它就是第k個最小的數;如果它的位置小於k-1,那麽說明第k個小的元素一定在數組的右半部分,采用遞歸的方法在數組的右半部分繼續查找;否則第k個小的元素在數組的左半部分,采用遞歸的方法在左半部分數組中繼續查找。
  • 示例如下:
技術分享圖片

技術分享圖片

  • 表面上看起來這種方法還是在對數組進行排序,但是它比排序法的效率高,主要原因是當在數組右半部分遞歸查找時,完全不需要關註左半部分數組的順序,因此省略了對左半部分數組的排序。因此,這種方法可以被看作一種“剪枝”方法,不斷縮小問題的規模,直到找到第k個小的元素。
8.5.8 如何找出數組中只出現一次的數字
  • 問題描述:一個整型數組裏除了一個數字之外,其他數字都出現了兩次。找出這個只出現1次的數字。要求時間復雜度是0(n),空間復雜度是0(1)。如果本題對時間復雜度沒有要求,最容易想到的方法就是先對這個整型數組排序,然後從第一個數字開始遍歷,比較相鄰的兩個數,從而找出這個只出現1次的數字,這種方法的時間復雜度最快為0( nlogn)。
  • 由於時間復雜度與空間復雜度的限制,該方法不可取,因此需要一種更高效的方式。題目強調只有一個數字出現1次,其他數字出現了兩次,首先想到的是異或運算,根據異或運算的定義可知,任何一個數字異或它自己都等於0,所以,如果從頭到尾依次異或數組中的每一個數字,那些出現兩次的數字全部在異或中會被抵消掉,最終的結果剛好是這個只出現1次的數字。示例如下:
  • 技術分享圖片

  • 引申:如果題目改為數組A中,一個整型數組裏除了-一個數字之外,其他數字都出現了3次,那麽如何找出這個數?上述異或運算的方法只適用於其他數字出現的次數為偶數的情況,如果其他數字出現的次數為奇數,,上述介紹的方法則不再適用。如果數組中的所有數都出現n次,那麽這個數組中的所有數對應的二進制數中,各個位,上的1出現的個數均可以被n整除。以n=3為例,假如數組中有如下元素:{1,1,1,2,2,2}, 它們對應的二進制表示為01,01,01,10,10,10。顯然,這個數組中的所有數字對應的二進制數中第0位有3個1,第1位有3個1。對於本題而言,假設出現一次的這個數為a,那麽去掉a後其他所有數字對應的二進制數的每個位置出現1的個數為3的倍數。因此可以對數組中的所有數字對應的二進制數中各個位置上1的個數對3取余數,就可以得到出現1次的這個數的二進制表示,從而可以找出這個數。示例如下:
  • 技術分享圖片

8.5.9 如何找出數組中唯的重復元素
  • 問題描述:數組a[N],1 ~N-1這N-1個數存放在a[N]中,其中某個數重復1次。寫一個函數,找出被重復的數字。要求每個數組元素只能訪問1次,並且不用輔助存儲空間。由於題目要求每個數組元素只能訪問1次,且不用輔助存儲空間,因此可以從原理上人手,采用數學求和法,因為只有一個數字重復1次,而又是連續的,根據累加和原理,對數組的所有項求和,然後減去1 ~ N-1的和,即為所求的重復數。
  • 示例如下:
  • 技術分享圖片

  • 如果題目沒有要求每個數組元素只能訪問1次,且不允許使用輔助存儲空間,還可以用異或法和位圖法來求解。
  • 1. 異或法
  • 根據異或法的計算方式,每兩個相異的數執行異或運算之後,結果為1;每兩個相同的數執行異或運算之後,結果為0,所以,數組a[N]中的N個數異或結果與1 ~ N-1異或的結果再做異或運算,得到的值即為所求。設重復數為A,其余N-2個數異或結果為B,N個數異或結果為A^A^B,1 ~ N-1異或結果為A^B,由於異或滿足交換律和結合律,且X^X=0, 0^X=X,則有(A^B)^(A^A^B) =A^B^B=A。
  • 示例如下:
技術分享圖片


  • 2. 空間換時間法
  • 申請長度為N-1的整型數組flag並初始化為0,然後從頭開始遍歷數組a,取每個數組元素a[i]的值,將其對應的數組flag 中的元素賦值為1,如果已經置過1,那麽該數就是重復的數。
  • 示例如下:
技術分享圖片 技術分享圖片


  • 此題可以進行-一個變形:取值為[1,n-1] 含n個元素的整數數組,至少存在一個重復數,即可能存在多個重復數,O(n)時間內找出其中任意一個重復數,例如,array[] ={1,2,2,4,5,4},則2和4均是重復元素。
    • 方案一:位圖法。使用大小為n的位圖,記錄每個元素是否已經出現過,一-旦遇到一個已經出現過的元素,則直接將之輸出。該方法的時間復雜度是O(n),空間復雜度為0(n)。
    • 方案二:數組排序法。先對數組進行計數排序,然後順序掃描整個數組,一旦遇到一個已出現的元素,則直接將之輸出。該方法的時間復雜度為O(n),空間復雜度為O(n)。
8.5.10 如何用遞歸方法求個整數數組的最大元素
  • 對於本題而言,最容易實現的方法為對數組進行遍歷,定義一個變量max為數組的第一個元素,然後從第二個元素開始遍歷,在遍歷過程中,每個元素都與max的值進行比較,若該元素的值比max的值大,則把該元素的值賦給max。當遍歷完數組後,最大值也就求出來了。 而使用遞歸方法求解的主要 思路為:遞歸的求解“數組第一 一個元素”與“數組中其他元素組成的子數組的最大值”的最大值。
  • 示例如下:
技術分享圖片


8.5.11 如何求數對之差的最大值
  • 問題描述:數組中的一個數字減去它右邊子數組中的一一個數字可以得到一一個差值,求所有可能的差值中的最大值,例如,數組{1,4,17,3,2,9}中,最大的差值為17-2=15。
  • 方法一:“蠻力”法。“蠻力”法也是最容易想到的方法,
  • 其原理如下:
  • 首先,遍歷數組,找到所有可能的差值;其次,從所有差值中找出最大值。具體實現方法為:針對數組a中的每個元素a[i](0<i<n-1),求所有a[i] -a[j](i<j<n)的值中的最大值。
  • 示例如下:
技術分享圖片


  • 方法二:二分法;
  • 方法三:動態規劃;
8.5.12 如何求絕對值最小的數
  • 二分法
8.5.14 如何制定數字在數組中第一次出現的位置
  • 二分查找法
  • 暴力法
8.5.15 如何對數組的兩個子有序段進行合並
  • 實現思路:首先,遍歷數組中”下標為0~ mid-1的元素,將遍歷到的元素的值與a[ mid ]進行比較,當遍歷到a[i](0<=i<= mid-1)時,如果滿足a[ mid] <a[門], 那麽交換a[i門]與a[mid]的值。接著找到交換後的a[mid]在a[mid,num-1]中的具體位置(在a[mid,num-1]中進行插入排序),實現方法為:遍歷a[ mid~ num-2],如果a[ mid+1] < a[ mid],那麽交換a[ mid]與a[mid +1]的位置。
8.5.16 如何計算兩個有序整型數組的交集
  • 二路歸並。
  • 順序遍歷法。
  • 散列法。
8.5.17 如何判斷一個數組中數值是否連續相鄰 8.5.18如何求解數組中反序對的個數
  • 問題描述:給定一個數組a,如果a[i] >a[j](i<j),那麽a[i]與a[j]被稱為一個反序,例如,給定數組{1,5,3,2,6}, 共有(5,3)、(5,2)和(3,2)三個反序對。
  • 方法一:“蠻力”法。最容易想到的方法是對數組中的每一一個數字,遍歷它後面的所有數字,如果後面的數字比它小,那麽就找到一個逆序對,實現代碼如下:
  • 方法二:分治歸並法。可以參考歸並排序的方法,在歸並排序的基礎上額外使用- -. 個計數器來記錄逆序對的個數。’下 面以數組序列{5,8,3,6} 為例,說明計數的方法。
8.6 字符串 8.6.1 如何實現字符串的反轉
  • 這道題的解決方法比較簡單,只需要進行兩次字符反轉的操作即可,第一次對整個字符串中的字符進行反轉,反轉結果為:“uoy era woh",通過這一- 次的反轉已經實現了單詞順序的反轉,只不過每個單詞中字符的順序反了,接下來只需要對每個單詞進行字符反轉即可得到想要的結果:“you are how"。
8.6.2 如何判斷兩個字符串是否由相同的字符組成
  • 問題描述:由相同的字符組成是指組成兩個字符串的字母以及各個字母的個數是一樣 的,只是排列順序不同而已,例如,“ aaabbe”與“abcbaaa”就由相同的字符組成的。下面講述判斷給定的兩個字符串是否由相同的字符組成的方法。
  • 方法一:排序法。最容易想到的方法就是對兩個字符串中的字符進行排序,比較兩個排序後的字符串是否相等。若相等,則表明它們是由相同的字符組成的,否則,則表明它們是由不同的字符組成的。
  • 方法二:空間換時間。在算法設計中,經常會采用空間換時間的方法以降低時間復雜度,即通過增加額外的存儲空間來達到優化算法的效果。就本題而言,假設字符串中只使用ASCII字符,由於ASCII字符共有266個( 對應的編碼為0 ~ 255),在實現時可以通過申請大小為266的數組來記錄各個字符出現的個數,並初始化為0,然後遍歷第-一個字符串,將字符串中字符對應的ASCII碼值作為數組”下標,把對應數組的元素加1,然後遍歷第二個字符串,把數組中對應的元素值-1。如果最後數組中各個元素的值都為0,說明這兩個字符串是由相同的字符組成的;否則,說明這兩個字符串是由不同的字符組成的。
8.6.3 如何刪除字符串中重復的字符
  • 問題描述:刪除字符串中重復的字符,例如,“good"去掉重復的字符後就變為“god"。
  • 方法一:“蠻力”法。最簡單的方法就是把這個字符串看作-一個字符數組,對該數組使用雙重循環進行遍歷,如果發現有重復的字符,就把該字符置為‘\0’,最後再把這個字符數組中的所有‘\0’去掉,此時得到的字符串就是刪除重復字符後的目標字符串。
  • 方法二:空間換時間。在算法中經常會采用空間換時間的方法。對於這個問題,也可以采取這種方法。其主要思路如下:由於常見的字符只有256個,可以假設這道題字符串中不同的字符個數最多為256個,那麽可以申請一個大小為256的int類型的數組來記錄每個字符出現的次數,初始化都為0,把這個字符的編碼作為數組的下標,在遍歷字符數組時,如果這個字符出現的次數為0,那麽把它置為1;如果這個字符出現的次數為1,說明這個字符在前面已經出現過了,就可以把這個字符置為\ 0‘,最後去掉所有‘\0’,就實現了去重的目的。采用這種方法只需要對字符數組進行--次遍歷即可,因此時間復雜度為O(n),但是需要額外申請256大小的空間。由於申請的數組用來記錄一個字符是否出現,只需要lbit就能實現這個功能,因此作為更好的一一種方案,可以只申請大小為8的int類型的數組,由於每個int類型占32bit,因此大小為8的數組總共為256bit,用1bit來表示-一個字符是否已經出現過可以達到同樣的目的。
  • 方法三:正則表達式。在Java語言中,利用正則表達式也可以達到同樣的目的: (? s)(. )(?=. *\\1)。
8.6.4 如何統計行字符中有多少個單詞
  • 單詞的數目可以由空格出現的次數決定(連續的若於個空格作為出現一-次空格; 一行開頭的空格不統計在內)。若測出某一個字符為非空格,而它的前面的字符是空格,則表示“新的單詞開始了”,此時使單詞計數器count值加1;若當前字符為非空格而其前面的字符也是非空格,則意味著仍然是原來那個單詞的繼續,count 值不應再累加1。前面一個字符是否空格可以從word的值看出來,若word等於0,則表示前一個字符是空格;若word等於1,意味著前一個字符為非空格。
8.6.5 如何按要求打印數組的排列情況 8.6.6 如何輸出字符串的所有組合
  • 暴力法
  • 遞歸
8.7 二叉樹
  • 二叉樹是一種非常 常見並實用的數據結構, 它結合了有序數組與鏈表的優點,在二叉樹中查找數據與在數組中查找數據一樣快,在二叉樹中添加刪除數據的速度也與在鏈表中一樣高效,所以,有關二叉樹的相關技術- -直是程序員面試筆試中必考的知識點。
8.7.1 二叉樹的基本概念 技術分享圖片

技術分享圖片

技術分享圖片

8.7.2 如何實現:叉排序樹
  • 二叉排序樹又稱二叉查找樹。它或者是一棵空樹,或者是具有下列性質的二叉樹:①如果左子樹不空,那麽左子樹上所有結點的值均小於它的根結點的值;②如果右子樹不空,那麽右子樹上所有結點的值均大於它的根結點的值;③左、右子樹也分別為二叉排序樹。由於二叉樹具有有序的特定,因此在筆試或面試過程中經常會出現二叉排序樹相關的題目。
8.7.3 如何層序遍歷二叉樹
  • 可以使用隊列來實現二叉樹的層序遍歷。其主要思路如下:先將根結點放入隊列中,然後每次都從隊列中取出一個結點打印該結點的值,若這個結點有子結點,則將它的子結點放入隊列尾,直到隊列為空。
8.7.5 如何求二叉樹中結點的最大距離
  • 問題描述:結點的距離是指這兩個結點之間邊的個數。寫一個程序求一棵二叉樹中相距最遠的兩個結點之間的距離。一般而言,對二叉樹的操作通過遞歸方法實現比較容易。求最大距離的主要思想如下:首先,求左子樹距根結點的最大距離,記為leftMaxDistance;其次,求右子樹距根結點的最大距離,記為rightMaxDistance,那麽二叉樹中結點的最大距離maxDistance滿足maxDistance = leftMaxDistance + rightMaxDistance。
8.8 其他 8.8.1 如何消除嵌套的括號
  • 問題描述:給定-一個如下格式的字符串(1,(2,3),(4,(5,6),7)), 括號內的元素可以是數字,也可以是另一個括號,實現-一個算法以消除嵌套的括號,例如,把上面的表達式變成(1,2,3,4,5,6,7), 若表達式有誤,則報錯。從問題描述可以看出,這道題要求實現兩個功能:一-是判斷表達式是否正確;二是消除表達式中嵌套的括號。對於判定表達式是否正確這個問題,可以從如下幾個方面來人手:首先,表達式中只有數字、逗號和括號這幾種字符,如果有其他字符出現則是非法表達式;其次,判斷括號是否匹配,若碰到“(”,則把括號的計數器值加1;如果碰到“)”,此時再判斷計數器值是否大於1,若是,則把計數器減1,否則為非法表達式。當遍歷完表達式後,若括號計數器值為0,則說明括號是配對出現的,否則說明括號不配對,則表達式為非法表達式。
8.8.2 如何不使用比較運算就可以求出兩個數的最大值與最小值
  • 通常來講,在求兩個數中的最大值或最小值時,最常用的方法就是比較大小。下面給出一種不需比較大小就可以求出兩個數中的最大值與最小值的方法,該方法用到了- ~種比較巧妙的數學方法,即最大值Max(a,b) =(a+b+|a-b|)/2,最小值Min(a,b) =(a+b- |a-b| )/2。當然,這種方法存在著一個問題,即當a與b的值非常大時,會出現數據溢出的情況,解決的辦法就是在計算時把a與b的值轉換為長整型,從而可以避免溢出的發生。示例如下:

8 數據結構與算法