程式設計師進階之路之面試題與筆試題集錦(一)

markriver發表於2021-09-09

一、資料結構-演算法的時間複雜度和空間複雜度

在程式設計題之前,首先我們先來聊聊時間複雜度:

演算法複雜度分為時間複雜度和空間複雜度。其作用: 時間複雜度是指執行演算法所需要的計算工作量;而空間複雜度是指執行這個演算法所需要的記憶體空間。(演算法的複雜性體現在執行該演算法時的計算機所需資源的多少上,計算機資源最重要的是時間和空間(即暫存器)資源,因此複雜度分為時間和空間複雜度)。 
簡單理解: 
(1)時間複雜度:執行這個演算法需要消耗多少時間。 
時間複雜度:在電腦科學中,演算法的時間複雜度是一個函式,它定量描述了該演算法的執行時間。這是一個關於代表演算法輸入值的字串的長度的函式。時間複雜度常用大O符號表述,不包括這個函式的低階項和首項係數。 
(2)空間複雜度:這個演算法需要佔用多少記憶體空間。 
空間複雜度(Space Complexity) 是對一個演算法在執行過程中臨時佔用儲存空間大小的量度,記做 S(n)=O(f(n)) ,其中n為問題的規模。利用演算法的空間複雜度,可以對演算法的執行所需要的記憶體空間有個預先估計。 
  一個演算法執行時除了需要儲存本身所使用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的工作單元和儲存一些計算所需的輔助空間。演算法執行時所需的儲存空間包括以下兩部分。   
(1)固定部分。這部分空間的大小與輸入/輸出的資料的個數、數值無關。主要包括指令空間(即程式碼空間)、資料空間(常量、簡單變數)等所佔的空間。這部分屬於靜態空間。 
(2)可變空間,這部分空間的主要包括動態分配的空間,以及遞迴棧所需的空間等。這部分的空間大小與演算法有關。

1.1 演算法的時間複雜度

(1)語句頻度T(n): 一個演算法執行所花費的時間,從理論上是不能算出來的,必須上機執行測試才能知道。但我們不可能對每個演算法都上機測試,只需知道哪個演算法花費的時間多,哪個演算法花費的時間少就可以了。而且一個演算法花費的時間與演算法中的基本操作語句的執行次數成正比例,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的語句執行次數稱為語句頻度,記為T(n)。

(2)時間複雜度: 在剛才提到的語句頻度中,n稱為問題的規模,當n不斷變化時,語句頻度T(n)也會不斷變化。但有時我們想知道它的變化呈現什麼規律。為此,我們引入時間複雜度概念。 一般情況下,演算法中的基本操作語句的重複執行次數是問題規模n的某個函式,用T(n)表示,若有某個輔助函式f(n),使得當n趨近於無窮大時,T(n) / f(n) 的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函式。記作 T(n)=O( f(n) ),稱O( f(n) ) 為演算法的漸進時間複雜度,簡稱時間複雜度。 
  T(n) 不同,但時間複雜度可能相同。 如:T(n)=n²+5n+6 與 T(n)=3n²+3n+2 它們的T(n) 不同,但時間複雜度相同,都為O(n²)。

(3)常見的時間複雜度有:常數階O(1),對數階O(log2n),線性階O(n),線性對數階O(nlog2n),平方階O(n2),立方階O(n3), k次方階O(nk),指數階O(2n)。隨著問題規模n的不斷增大,上述時間複雜度不斷增大,演算法的執行效率越低。

(4)平均時間複雜度和最壞時間複雜度:

    平均時間複雜度是指所有可能的輸入例項均以等機率出現的情況下,該演算法的執行時間。

 最壞情況下的時間複雜度稱最壞時間複雜度。一般討論的時間複雜度均是最壞情況下的時間複雜度。 這樣做的原因是:最壞情況下的時間複雜度是演算法在任何輸入例項上執行時間的界限,這就保證了演算法的執行時間不會比最壞情況更長。 
(5)如何求時間複雜度:  
【1】如果演算法的執行時間不隨著問題規模n的增加而增長,即使演算法中有上千條語句,其執行時間也不過是一個較大的常數。此類演算法的時間複雜度是O(1)。

    public static void main(String[] args) {        int x = 91;        int y = 100;        while (y > 0) {            if (x > 100) {
                x = x - 10;
                y--;
            } else {
                x++;
            }
        }
    }123456789101112

該演算法的時間複雜度為:O(1) 
這個程式看起來有點嚇人,總共迴圈執行了1100次,但是我們看到n沒有? 
沒。這段程式的執行是和n無關的, 
就算它再迴圈一萬年,我們也不管他,只是一個常數階的函式 
【2】當有若干個迴圈語句時,演算法的時間複雜度是由巢狀層數最多的迴圈語句中最內層語句的頻度f(n)決定的。

         int x = 1;         for (int i = 1; i 

該演算法的時間複雜度為:O(n3) 
該程式段中頻度最大的語句是第5行的語句,內迴圈的執行次數雖然與問題規模n沒有直接關係,但是卻與外層迴圈的變數取值有關,而最外層迴圈的次數直接與n有關,因此該程式段的時間複雜度為 O(n3)

1         int i = n - 1;2         while (i >= 0 && (A[i] != k)) {3             i--;4         }5         return i;12345

該演算法的時間複雜度為:O(n)    
此演算法中第3行語句的頻度不僅與問題規模n有關,還與輸入例項A中的各元素取值和k的取值有關:如果A中沒有與k相等的元素,那麼第3行語句的頻度為 f(n)=n ,該程式段的時間複雜度為 O(n) 
 (6)用時間複雜度來評價演算法的效能 
    用兩個演算法A1和A2求解同一問題,時間複雜度分別是O(100n2),O(5n3) 
    (1) 5n3/100n2=n/20 ,當輸入量n<20時,100n2 > 5n3 ,這時A2花費的時間較少。 
    (2)隨著問題規模n的增大,兩個演算法的時間開銷之比 5n3/100n2=n/20 也隨著增大。即當問題規模較大時,演算法A1比演算法A2要高效的多。它們的漸近時間複雜度O(n2)和O(n3) 評價了這兩個演算法在時間方面的效能。在演算法分析時,往往對演算法的時間複雜度和漸近時間複雜度不予區分,而經常是將漸近時間複雜度 O(f(n)) 簡稱為時間複雜度,其中的f(n)一般是演算法中頻度最大的語句頻度。 
 (7)小結 
演算法的時間複雜度和兩個因素有關:演算法中的最大巢狀迴圈層數;最內層迴圈結構中迴圈的次數。

一般來說,具有多項式時間複雜度的演算法是可以接受的;具有指數(不是對數)時間複雜度的演算法,只有當n足夠小時才可以使用。一般效率較好的演算法要控制在O(log2n) 或者 O(n) 
通常:容易計算的方法是看看有幾重for迴圈,只有一重則時間複雜度為O(n),二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查詢,如果一個for迴圈套一個二分,那麼時間複雜度則為O(nlogn)

1.2常見演算法時間複雜度整理

各種常用排序演算法









類別

排序方法

時間複雜度

空間複雜度

穩定性

複雜性

特點



最好

平均

最壞

輔助儲存


簡單




插入

排序

直接插入

O(N)

O(N2)

O(N2)

O(1)

穩定

簡單 


希爾排序

O(N)

O(N1.3)

O(N2)

O(1)

不穩定

複雜



選擇

排序

直接選擇

O(N)

O(N2)

O(N2)

O(1)

不穩定



堆排序

O(N*log2N)

O(N*log2N)

O(N*log2N)

O(1)

不穩定

複雜



交換

排序

氣泡排序

O(N)

O(N2)

O(N2)

O(1)

穩定

簡單

1、氣泡排序是一種用時間換空間的排序方法,n小時好
2、最壞情況是把順序的排列變成逆序,或者把逆序的數列變成順序,最差時間複雜度O(N^2)只是表示其操作次數的數量級
3、最好的情況是資料本來就有序,複雜度為O(n)

快速排序

O(N*log2N)

O(N*log2N) 

O(N2)

O(log2n)~O(n) 

不穩定

複雜

1、n大時好,快速排序比較佔用記憶體,記憶體隨n的增大而增大,但卻是效率高不穩定的排序演算法。
2、劃分之後一邊是一個,一邊是n-1個,
這種極端情況的時間複雜度就是O(N^2)
3、最好的情況是每次都能均勻的劃分序列,O(N*log2N)


歸併排序

O(N*log2N) 

O(N*log2N) 

O(N*log2N) 

O(n)

穩定

複雜

1、n大時好,歸併比較佔用記憶體,記憶體隨n的增大而增大,但卻是效率高且穩定的排序演算法。


基數排序

O(d(r+n))

O(d(r+n))

O(d(r+n))

O(rd+n)

穩定

複雜



注:r代表關鍵字基數,d代表長度,n代表關鍵字個數









注:

1、歸併排序每次遞迴都要用到一個輔助表,長度與待排序的表長度相同,雖然遞迴次數是O(log2n),但每次遞迴都會釋放掉所佔的輔助空間,

2、快速排序空間複雜度只是在通常情況下才為O(log2n),如果是最壞情況的話,很顯然就要O(n)的空間了。當然,可以透過隨機化選擇pivot來將空間複雜度降低到O(log2n)。

1.3 演算法的空間複雜度計算

舉例分析演算法的空間複雜度: 
public void reserse(int[] a, int[] b) { 
int n = a.length; 
for (int i = 0; i b[i] = a[n - 1 - i]; 


參考前面的空間複雜度定義,上方的程式碼中,當程式呼叫 reserse() 方法時,要分配的記憶體空間包括:引用a、引用b、區域性變數n、區域性變數i 
因此 f(n)=4 ,4為常量。所以該演算法的空間複雜度 S(n)=O(1)

二、陣列方面

注意*****以下程式在python3上進行執行1
1. 陣列中重複的數字

在一個長度為n的陣列裡的所有數字都在0到n-1的範圍內。 陣列中某些數字是重複的,但不知道有幾個數字是重複的。也不知道每個數字重複幾次。請找出陣列中任意一個重複的數字。 例如,如果輸入長度為7的陣列【2,3,1,0,2,5,3】,那麼對應的輸出是第一個重複的數字2。 
相似題型解答:統計一個數字在排序陣列中出現的次數。

#官方答案# -*- coding:utf-8 -*-import collectionsclass Solution:
    # 這裡要特別注意~找到任意重複的一個值並賦值到duplication
    # 函式返回True/False
    def duplicate(self, numbers, duplication):
        # write code here
        flag=False
        c=collections.Counter(numbers)        for k,v in c.items():            if v>1:
                duplication=k
                flag=True
                break
        return flag,duplication12345678910111213141516

執行形式

numbers=[2,3,1,0,2,5,3]duplication=-1
Solution().duplicate(numbers,duplication)123

在python中也可以對numbera進行處理

b=[]for x in list(set(numbers)):
       b.append(numbers.count(x))
 print(max(b))1234
2. 連續子陣列的最大和

Kadane演算法又被稱為掃描法,為動態規劃(dynamic programming)的一個典型應用。我們用DP來解決最大子陣列和問題:對於陣列a,用ci標記子陣列a[0..i]的最大和,那麼則有

ci=max{ai,ci−1+ai}ci=max{ai,ci−1+ai}

# -*- coding:utf-8 -*-#法一:動態規劃 O(n),並輸出了最大和元素所在的索引class Solution:
    def FindGreatestSumOfSubArray(self, array):
        if not array:            return 0
        start=[]
        res,cur=array[0],array[0]        for i in range(1,len(array)):

            cur+=array[i]            #每次對比計算和和原來數值的大小
            res=max(res,cur)            #判斷新的累加和是否小於0,小於則賦予0
            if cur

測試

array=[1,2,-4,2,6,8]
Solution().FindGreatestSumOfSubArray(array)12

法二:窮舉法 
i, j的for迴圈表示x[i..j],k的for迴圈用來計算x[i..j]之和。

maxsofar = 0for i = [0, n)    for j = [i, n)        sum = 0
        for k = [i, j]            sum += x[k]        /* sum is sum of x[i..j] */
        maxsofar = max(maxsofar, sum)12345678

有三層迴圈,窮舉法的時間複雜度為O(n3)O(n3)

氣泡排序

主要是拿一個數與列表中所有的數進行比對,若比此數大(或者小),就交換位置

執行一次for迴圈,遍歷第一個元素,放到最後的位置

l=[5,3,6,2,1,4,8,7,9]for j in range(len(l)-1):    if l[j] > l[j+1]:
        l[j],l[j+1] = l[j+1],l[j]print(l)12345

執行2次迴圈,在第一次的基礎之上,每次執行一個元素與其他元素進行比對,之後插入相應的末尾

l=[5,3,6,2,1,4,8,7,9]for i in range(len(l)-1):    for j in range(len(l)-1-i):        if l[j] > l[j+1]:
            l[j],l[j+1] = l[j+1],l[j]print(l)12345678

插入排序

有一個已經有序的資料序列,要求在這個已經排好的資料序列中插入一個數,但要求插入後此資料序列仍然有序 
基本思想為:每步將一個待排序的紀錄,按其關鍵碼值的大小插入前面已經排序的檔案中適當位置上,直到全部插入完為止。 
圖片描述 
插入排序就是用一個數與一個已排好序的序列進行比對,從右向左進行。 
例:5與3進行比較,5>3,將5與3不進行交換。l=[3,5], 
此時再進行排序。4

l=[1,5,4,7,9,3,2,6,8]#索引為0,注意從1開始for i in range(1,len(l)):#步長為-1
    for j in range(i,0,-1):        if l[j] 

二分法查詢

二分查詢又稱折半查詢,優點是比較次數少,查詢速度快,平均效能好;其缺點是要求待查表為有序表,且插入刪除困難。 
此方法適用於不經常變動而查詢頻繁的有序列表。 
首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查詢關鍵字比較,如果兩者相等,則查詢成功;否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查詢關鍵字,則進一步查詢前一子表,否則進一步查詢後一子表。重複以上過程,直到找到滿足條件的記錄,使查詢成功,或直到子表不存在為止,此時查詢不成功。 
例項:

#encoding:utf-8l = [1, 2, 3, 4, 5, 6, 7, 8, 9]
find_num = int(input('請輸入一個數字:'))
start = 0end = len(l) - 1while True:    middle = (start + end) // 2
    if find_num == l[middle]:
        print('找到了!索引是:', middle)
        break
    elif find_num > l[middle]:
        start = middle + 1
    elif find_num  end:
        print('沒找到!', find_num)
        break123456789101112131415161718


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/818/viewspace-2803299/,如需轉載,請註明出處,否則將追究法律責任。

相關文章