2021秋招面試計算機基礎總結 - 演算法,資料結構,設計模式,Linux

Borris發表於2020-09-24

排序演算法

  • 分類

    • 排序演算法可以分為內部排序外部排序,在記憶體中進行的排序稱為內部排序,當要排序的資料量很大時無法全部拷貝到記憶體,需要使用外存進行排序,這種排序稱為外部排序。
    • 內部排序包括比較排序和非比較排序,比較排序包括插入排序、選擇排序、交換排序和歸併排序,非比較排序包括計數排序、基數排序和桶排序。其中插入排序又包括直接插入排序和希爾排序,選擇排序包括直接選擇排序和堆排序,交換排序包括氣泡排序和快速排序。
  • 穩定性:排序前後兩個相等的數相對位置不變,則演算法穩定。

    • 不穩定:堆排序、快速排序、希爾排序、直接選擇排序
    • 穩定:基數排序、氣泡排序、直接插入排序、折半插入排序、歸併排序的排序演算法。

直接插入排序

一種穩定的排序,平均時間複雜度和最差時間複雜度均為 O(n²),當元素基本有序時的最好時間複雜度為O(n),空間複雜度為 O(1)。

  • 基本原理:
    每一趟將一個待排序的記錄,按其關鍵字的大小插入到已經排好序的一組記錄的適當位置上,直到所有待排序記錄全部插入為止。適用於待排序記錄較少或基本有序的情況。
public class InsertSort {
    public static int[] insertSort(int[] arr) {
        if(arr == null || arr.length < 2)
            return arr;

        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int temp = arr[i];
            int k = i - 1;
            while(k >= 0 && arr[k] > temp)
                k--;
            //騰出位置插進去,要插的位置是 k + 1;
            for(int j = i ; j > k + 1; j--)
                arr[j] = arr[j-1];
            //插進去
            arr[k+1] = temp;
        }
        return arr;
    }
}

希爾排序

屬於插入排序,又稱縮小增量排序,是對直接插入排序的一種改進,並且是一種不穩定的排序,平均時間複雜度為O(n^1.3),最差時間複雜度為 O(n²),最好時間複雜度為 O(n),空間複雜度為 O(1)。

  • 基本原理是:
    把記錄按下標的一定增量分組,對每組元素進行直接插入排序,每次排序後減小增量以重新分組,當增量減至 1 時,排序就完成了。適用於中等規模的資料量,對規模非常大的資料量不是最佳選擇。
public class ShellSort {
    public static int[] shellSort(int arr[]) {
        if (arr == null || arr.length < 2) return arr;
        int n = arr.length;
        // 對每組間隔為 h的分組進行排序,剛開始 h = n / 2;
        for (int h = n / 2; h > 0; h /= 2) {
            //對各個區域性分組進行插入排序
            for (int i = h; i < n; i++) {
                // 將arr[i] 插入到所在分組的正確位置上
                insertI(arr, h, i);
            }
     }
     return arr;
    }

    /**
     * 將arr[i]插入到所在分組的正確位置上
     * arr[i]] 所在的分組為 ... arr[i-2*h],arr[i-h], arr[i+h] ...
     */
    private static void insertI(int[] arr, int h, int i) {
        int temp = arr[i];
        int k;
        for (k = i - h; k > 0 && temp < arr[k]; k -= h) {
            arr[k + h] = arr[k];
        }
        arr[k + h] = temp;
    }
}

選擇排序

一種不穩定的排序,任何情況下時間複雜度都是 O(n²),空間複雜度為 O(1)。

  • 基本工作原理:
    首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
public class SelectSort {
    public static int[] selectSort(int[] a) {
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            int min = i;
            for (int j = i + 1; j < n; j++) {
                if(a[min] > a[j]) min = j;
            }
            //交換
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        return a;
    }
}

堆排序

屬於選擇排序,是對直接選擇排序的改進,並且是一種不穩定的排序,任何情況時間複雜度都為 O(nlogn),空間複雜度為 O(1)。初始化建堆O(n);重排序建堆O(nlogn)。

  • 基本原理是:將待排序記錄看作完全二叉樹。
    • 將無需序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆
    • 將堆頂元素與末尾元素交換,將最大元素”沉”到陣列末端
    • 重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
public class Head {
    // 堆排序
    public static int[] headSort(int[] arr) {
        int n = arr.length;
        //構建大頂堆
        for (int i = (n - 2) / 2; i >= 0; i--) {
            downAdjust(arr, i, n - 1);
        }
        //進行堆排序
        for (int i = n - 1; i >= 1; i--) {
            // 把堆頂元素與最後一個元素交換
            int temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            // 把打亂的堆進行調整,恢復堆的特性
            downAdjust(arr, 0, i - 1);
        }
        return arr;
    }

        //下沉操作
    public static void downAdjust(int[] arr, int parent, int n) {
        //臨時儲存要下沉的元素
        int temp = arr[parent];
        //定位左孩子節點的位置
        int child = 2 * parent + 1;
        //開始下沉
        while (child <= n) {
            // 如果右孩子節點比左孩子大,則定位到右孩子
            if(child + 1 <= n && arr[child] < arr[child + 1])
                child++;
            // 如果孩子節點小於或等於父節點,則下沉結束
            if (arr[child] <= temp ) break;
            // 父節點進行下沉
            arr[parent] = arr[child];
            parent = child;
            child = 2 * parent + 1;
        }
        arr[parent] = temp;
    }
}

氣泡排序

氣泡排序屬於交換排序,是一種不穩定的排序,平均時間複雜度和最壞時間複雜度均為 O(n²),當元素基本有序時的最好時間複雜度為O(n),空間複雜度為 O(1)。
基本原理是:它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行,直到沒有再需要交換,也就是說該數列已經排序完成。

public class BubbleSort {
    public static int[] bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return arr;
        }
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            boolean flag = true;
            for (int j = 0; j < n -i - 1; j++) {
                if (arr[j + 1] < arr[j]) {
                    flag = false;
                    int t = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = t;
                }
            }
            //一趟下來是否發生位置交換
            if(false)
                break;
        }
        return arr;
    }
}

快速排序

是一種不穩定的排序,平均時間複雜度和最好時間複雜度均為 O(nlogn),當元素基本有序時的最壞時間複雜度為O(n²),空間複雜度為 O(logn)。

  • 基本原理是:
    從陣列中選擇一個元素,作為中軸元素,然後把陣列中所有小於中軸元素的元素放在其左邊,所有大於或等於中軸元素的元素放在其右邊。
    (那麼此時中軸元素所處的位置的是有序的。也就是說,我們無需再移動中軸元素的位置。)
    從中軸元素那裡開始把大的陣列切割成左右兩個小的陣列(兩個陣列都不包含中軸元素),接著通過遞迴的方式,讓中軸元素左邊的陣列和右邊的陣列也重複同樣的操作,直到陣列的大小為1,此時每個元素都處於有序的位置。
public class QuickSort {
    public static int[] quickSort(int[] arr, int left, int right) {
        if (left < right) {
            //獲取中軸元素所處的位置
            int mid = partition(arr, left, right);
            //進行分割
            arr = quickSort(arr, left, mid - 1);
            arr = quickSort(arr, mid + 1, right);
        }
        return arr;
    }

    private static int partition(int[] arr, int left, int right) {
        //選取中軸元素
        int pivot = arr[left];
        int i = left + 1;
        int j = right;
        while (true) {
            // 向右找到第一個小於等於 pivot 的元素位置
            while (i <= j && arr[i] <= pivot) i++;
            // 向左找到第一個大於等於 pivot 的元素位置
            while(i <= j && arr[j] >= pivot ) j--;
            if(i >= j)
                break;
            //交換兩個元素的位置,使得左邊的元素不大於pivot,右邊的不小於pivot
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
        arr[left] = arr[j];
        // 使中軸元素處於有序的位置
        arr[j] = pivot;
        return j;
    }
}

歸併排序

歸併排序是基於歸併操作的排序演算法,是一種穩定的排序演算法,任何情況時間複雜度都為O(nlogn),空間複雜度為 O(n)。

  • 基本原理
    應用分治法將待排序序列分成兩部分,然後對兩部分分別遞迴排序,最後進行合併,使用一個輔助空間並設定兩個指標分別指向兩個有序序列的起始元素,將指標對應的較小元素新增到輔助空間,重複該步驟到某一序列到達末尾,然後將另一序列剩餘元素合併到輔助空間末尾。適用於資料量大且對穩定性有要求的情況。

遞迴:

public class MergeSort {
    // 歸併排序
    public static int[] mergeSort(int[] arr, int left, int right) {
        // 如果 left == right,表示陣列只有一個元素,則不用遞迴排序
        if (left < right) {
            // 把大的陣列分隔成兩個陣列
            int mid = (left + right) / 2;
            // 對左半部分進行排序
            arr = mergeSort(arr, left, mid);
            // 對右半部分進行排序
            arr = mergeSort(arr, mid + 1, right);
            //進行合併
            merge(arr, left, mid, right);
        }
        return arr;
    }

    // 合併函式,把兩個有序的陣列合並起來
    // arr[left..mif]表示一個陣列,arr[mid+1 .. right]表示一個陣列
    private static void merge(int[] arr, int left, int mid, int right) {
        //先用一個臨時陣列把他們合併彙總起來
        int[] a = new int[right - left + 1];
        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] < arr[j]) {
                a[k++] = arr[i++];
            } else {
                a[k++] = arr[j++];
            }
        }
        while(i <= mid) a[k++] = arr[i++];
        while(j <= right) a[k++] = arr[j++];
        // 把臨時陣列複製到原陣列
        for (i = 0; i < k; i++) {
            arr[left++] = a[i];
        }
    }
}

非遞迴:

public class MergeSort {
    // 非遞迴式的歸併排序
    public static int[] mergeSort(int[] arr) {
        int n = arr.length;
        // 子陣列的大小分別為1,2,4,8...
        // 剛開始合併的陣列大小是1,接著是2,接著4....
        for (int i = 1; i < n; i += i) {
            //進行陣列進行劃分
            int left = 0;
            int mid = left + i - 1;
            int right = mid + i;
            //進行合併,對陣列大小為 i 的陣列進行兩兩合併
            while (right < n) {
                // 合併函式和遞迴式的合併函式一樣
                merge(arr, left, mid, right);
                left = right + 1;
                mid = left + i - 1;
                right = mid + i;
            }
            // 還有一些被遺漏的陣列沒合併,千萬別忘了
            // 因為不可能每個字陣列的大小都剛好為 i
            if (left < n && mid < n) {
                merge(arr, left, mid, n - 1);
            }
        }
        return arr;
    }
}

設計模式的原則

開閉原則:物件導向設計中最基礎的設計原則,指一個軟體實體(類、模組、方法等)應該對擴充套件開放,對修改關閉。它強呼叫抽象構建框架,用實現擴充套件細節,提高程式碼的可複用性和可維護性。例如在版本更新時儘量不修改原始碼,但可以增加新功能。
單一職責原則:一個類、介面或方法只負責一個職責,可以提高程式碼可讀性和可維護性,降低程式碼複雜度以及變更引起的風險。
依賴倒置原則:程式應該依賴於抽象類或介面,而不是具體的實現類。可以降低程式碼的耦合度,提高系統的穩定性。
介面隔離原則:將不同功能定義在不同介面中實現介面隔離,避免了類依賴它不需要的介面,減少了介面之間依賴的冗餘性和複雜性。
里氏替換原則:對開閉原則的補充,規定了任何父類可以出現的地方子類都一定可以出現,可以約束繼承氾濫,加鍵程式健壯性。
迪米特原則:也叫最少知道原則,每個模組對其他模組都要儘可能少的瞭解和依賴,可以降低程式碼耦合度。
合成/聚合原則:儘量使用組合(has a)或聚合(contains a)而不是繼承關係達到軟體複用的目的,可以使系統更加靈活,降低耦合度。

設計模式的分類

建立型模式:提供了一種在建立物件的同時隱藏建立邏輯的方式,而不是使用 new 運算子直接例項化物件,這使得程式在判斷針對某個給定例項需要建立哪些物件時更加靈活。包括:工廠模式、抽象工廠模式、單例模式、建造者模式、原型模式。
結構型模式:通過類和介面之間的繼承和引用實現建立複雜結構物件的功能。包括:介面卡模式、橋接模式、過濾器模式、組合模式、裝飾器模式、外觀模式、享元模式、代理模式。
行為型模式:通過類之間不同的通訊方式實現不同的行為方式。包括:責任鏈模式、命名模式、直譯器模式、迭代器模式、中介者模式、備忘錄模式、觀察者模式、狀態模式、策略模式、模板模式、訪問者模式。

幾種模式的示例

簡單工廠模式

概念:由一個工廠物件建立例項,客戶端不需要關注建立邏輯,只需提供傳入工廠的引數。
場景:適用於工廠類負責建立物件較少的情況,缺點是如果要增加新產品,就需要修改工廠類的判斷邏輯,違背開閉原則。
舉例:

  • Calendar 類的 getInstance 方法,呼叫 createCalendar 方法根據不同的地區引數建立不同的日曆物件。
  • Spring 中的 BeanFactory,根據傳入一個唯一的標識來獲得 Bean 例項。

工廠方法模式

概念:定義一個建立物件的介面,讓介面的實現類決定建立哪種物件,讓類的例項化推遲到子類中進行。
場景:主要解決了產品擴充套件的問題,在簡單工廠模式中如果產品種類變多,工廠的職責會越來越多,不便於維護。
舉例:

  • Collection 介面中定義了一個抽象的 iterator 工廠方法,返回一個 Iterator 類的抽象產品。該方法通過 ArrayList 、HashMap 等具體工廠實現,返回 Itr、KeyIterator 等具體產品。
  • Spring 的 FactoryBean 介面的 getObject 方法。

抽象工廠模式

概念:提供一個建立一系列相關物件的介面,無需指定它們的具體類。缺點是不方便擴充套件產品族,並且增加了系統的抽象性和理解難度。
場景:主要用於系統的產品有多於一個的產品族,而系統只消費其中某一個產品族產品的情況。
舉例:java.sql.Connection 介面就是一個抽象工廠,其中包括很多抽象產品如 Statement、Blob、Savepoint 等。

單例模式

在任何情況下都只存在一個例項,構造方法必須是私有的、由自己建立一個靜態變數儲存例項,對外提供一個靜態公有方法獲取例項。
優點是記憶體中只有一個例項,減少了開銷;缺點是沒有抽象層,難以擴充套件,與單一職責原則衝突。
舉例:Spring 的 ApplicationContext 建立的 Bean 例項都是單例物件,還有 ServletContext、資料庫連線池等也都是單例模式。

餓漢式:類載入時就建立單例物件,執行緒安全,但不管是否使用都建立物件。可能會浪費記憶體。

public class Singleton {
    private Singleton(){}

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
}

懶漢式:在外部呼叫時才會載入,執行緒不安全,可以加鎖保證執行緒安全但效率低。

public class Singleton {
    private Singleton(){}

    private static Singleton instance;

    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

雙重檢查鎖:使用 volatile 以及多重檢查來減小鎖範圍,提升效率。

public class Singleton {
    private Singleton(){}
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

靜態內部類:同時解決餓漢式的記憶體浪費問題和懶漢式的執行緒安全問題。

public class Singleton {
    private Singleton(){}

    public static Singleton getInstance() {
        return StaticClass.instance;
    }

    private static class StaticClass {
        private static final Singleton instance = new Singleton();
    }
}

列舉:《Effective Java》提倡的方式,不僅能避免執行緒安全問題,還能防止反序列化重新建立新的物件,也能防止反射破解單例的問題。

public enum Singleton {
    INSTANCE;
}

代理模式

代理模式屬於結構型模式,為其他物件提供一種代理來控制對該物件的訪問。優點是可以增強目標物件的功能,降低程式碼耦合度;缺點是請求處理速度變慢,增加系統複雜度。
靜態代理:代理物件持有被代理物件的引用,呼叫代理物件方法時會呼叫被代理物件的方法,但是會增加其他邏輯。需要手動完成,在程式執行前就已經存在代理類的位元組碼檔案,代理類和被代理類的關係在執行前就已確定。 缺點是一個代理類只能為一個目標服務。
動態代理:動態代理在程式執行時通過反射建立具體的代理類,代理類和被代理類的關係在執行前是不確定的。動態代理的適用性更強,主要分為 JDK 動態代理和 CGLib 動態代理。

  • JDK 代理:
    通過 Proxy 的 newProxyInstance 方法獲得代理物件,需要三個引數:被代理類的介面、類載入器以及 InvocationHandler 物件,需要重寫 InvocationHandler 介面的 invoke 方法指明代理邏輯。
  • CGLib 代理:
    通過 Enhancer 物件的 create 方法獲取代理物件,需要通過 setSuperclass 方法設定代理類,以及 setCallback 方法指明代理邏輯(傳入一個MethodInterceptor 介面的實現類,具體代理邏輯宣告在 intercept 方法)。
    JDK 動態代理直接寫位元組碼,而 CGLib 動態代理使用 ASM 框架寫位元組碼, JDK 代理呼叫代理方法通過反射實現,而 GCLib 通過 FastClass 機制實現,為代理類和被代理類各生成一個類,該類為代理類和被代理類的方法分配一個 int 引數,呼叫方法時可以直接定位,效率更高。

裝飾器模式

概念:在不改變原有物件的基礎上將功能附加到物件,相比繼承更加靈活。

場景:在不想增加很多子類的前提下擴充套件一個類的功能。

舉例:java.io 包中,InputStream 通過 BufferedInputStream 增強為緩衝位元組輸入流。

和代理模式的區別:裝飾器模式的關注點在於給物件動態新增方法,而動態代理更注重物件的訪問控制。動態代理通常會在代理類中建立被代理物件的例項,而裝飾器模式會將被裝飾者作為構造方法的引數。

介面卡模式

概念:作為兩個不相容介面之間的橋樑,使原本由於介面不相容而不能一起工作的類可以一起工作。 缺點是過多使用介面卡會讓系統非常混亂,不易整體把握。

舉例:

  • java.io 包中,InputStream 通過 InputStreamReader 轉換為 Reader 字元輸入流。
  • Spring MVC 中的 HandlerAdapter,由於 handler 有很多種形式,包括 Controller、HttpRequestHandler、Servlet 等,但呼叫方式又是確定的,因此需要介面卡來進行處理,根據適配規則呼叫 handle 方法。
  • Arrays.asList 方法,將陣列轉換為對應的集合(不能使用修改集合的方法,因為返回的 ArrayList 是 Arrays 的一個內部類)。

和裝飾器模式的區別:介面卡模式沒有層級關係,介面卡和被適配者沒有必然連續,滿足 has-a 的關係,解決不相容的問題,是一種後置考慮;裝飾器模式具有層級關係,裝飾器與被裝飾者實現同一個介面,滿足 is-a 的關係,注重覆蓋和擴充套件,是一種前置考慮。

和代理模式的區別:介面卡模式主要改變所考慮物件的介面,而代理模式不能改變所代理類的介面。

策略模式

概念:定義了一系列演算法並封裝,之間可以互相替換。優點是演算法可以自由切換,可以避免使用多重條件判斷並且擴充套件性良好,缺點是策略類會增多並且所有策略類都需要對外暴露。

場景:主要解決在有多種演算法相似的情況下,使用 if/else 所帶來的難以維護。

舉例:

  • 集合框架中常用的 Comparator 就是一個抽象策略,一個類通過實現該介面並重寫 compare 方法成為具體策略類。
  • 執行緒池的拒絕策略。

模板模式

概念:使子類可以在不改變演算法結構的情況下重新定義演算法的某些步驟。優點是可以封裝固定不變的部分,擴充套件可變的部分;缺點是每一個不同實現都需要一個子類維護,會增加類的數量。

場景:適用於抽取子類重複程式碼到公共父類。

舉例:HttpServlet 定義了一套處理 HTTP 請求的模板,service 方法為模板方法,定義了處理HTTP請求的基本流程,doXXX 等方法為基本方法,根據請求方法的型別做相應的處理,子類可重寫這些方法。

觀察者模式

概念:也叫釋出訂閱模式,定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。缺點是如果被觀察者物件有很多的直接和間接觀察者的話通知很耗時, 如果存在迴圈依賴的話可能導致系統崩潰,另外觀察者無法知道目標物件具體是怎麼發生變化的。
場景:主要解決一個物件狀態改變給其他物件通知的問題。
舉例:ServletContextListener 能夠監聽 ServletContext 物件的生命週期,實際上就是監聽 Web 應用。當 Servlet 容器啟動 Web 應用時呼叫 contextInitialized 方法,終止時呼叫 contextDestroyed 方法。

部署專案用到的linux命令:
1.進入tomcat的bin目錄 cd /data/apache-tomcat-6.0.39/bin
2.檢視tomcat的程式 ps -ef | grep tomcat
3.殺死程式 kill -9 + 程式數
檢視程式 2.1、ps -ef | grep xx 2.2、ps -aux | grep xxx(-aux顯示所有狀態)
檢視埠:1、netstat -anp | grep 埠號(狀態為LISTEN表示被佔用)
4.啟動專案 sh startup.sh
5.永久刪除檔案
rm -rf * 刪除當前目錄下的所有檔案,這個命令很危險,應避免使用。所刪除的檔案,一般都不能恢復!;
rm -f 其中的,f引數 (f –force ) 忽略不存在的檔案,不顯示任何資訊
6.複製檔案 cp -Rf 原路徑/ 目的路徑/
7.壓縮資料夾
解壓:tar zxvf FileName.tar.gz
壓縮:tar zcvf FileName.tar.gz DirName
8.解壓(安裝zip命令)* unzip 壓縮包
9.移動 mv +路徑/檔案 +要移到的路徑
scp -r 目錄名 遠端計算機使用者名稱@遠端計算機的ip:遠端計算機存放該目錄的路徑
10.切換使用者 su 使用者名稱

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章