[-演算法篇-] 開篇前言

張風捷特烈發表於2019-04-16

零、前言

[1].從氣泡排序和快速排序引入演算法
[2].時間複雜度的引入
[3].空間複雜度的引入
[4].資料結構和演算法之間的雜談
複製程式碼
關於程式的執行
輸入: 原生可用資料 = 資料獲取 + 資料解析
處理:邏輯加工(演算法核心)
輸出:獲得預期資料
拿一個排序演算法來說:[輸入原始雜亂資料,通過邏輯加工,生成預期有序資料]
複製程式碼

一、從氣泡排序和快速排序開始說起

100W個隨機數,儲存到檔案中,使用時解析資料形成int陣列
問: 為什麼要存到檔案裡,直接在記憶體裡不就行了嗎?
---- 資料固化之後,保證原始資料不變且容易檢視和再加工

  • 排序之前的前3000個

排序之前的前3000個.png

  • 排序之前的前3000個

排序過後的前3000個.png


1、資料準備
1.1原始資料的生成

資料的來源可以多種多樣,這裡用最簡單的方式生成大批量資料,隨機100W個0~100W的數字

資料.png

public class NumMaker {
    public static void main(String[] args) throws IOException {
        String path = "J:\\sf\\data\\num.txt";
        int bound = 100 * 10000;
        initData(path, bound);
    }
    /**
     * 初始化資料
     *
     * @param path  檔案路徑
     * @param bound 資料個數
     * @throws IOException 
     */
    private static void initData(String path, int bound) throws IOException {
        Random random = new Random();
        FileHelper.mkFile(path);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bound; i++) {
            sb.append(random.nextInt(bound));
            if (i != bound - 1) {
                sb.append(",");
            }
        }
        FileWriter writer = new FileWriter(path);
        writer.write(sb.toString());
        writer.close();
    }
}

/**
 * 建立檔案
 * @param path 檔案路徑
 * @return 檔案是否被建立
 */
public static boolean mkFile(String path) {
    boolean success = true;
    File file = new File(path);//1.建立檔案物件
    if (file.exists()) {//2.判斷檔案是否存在
        return false;//已存在則返回
    }
    File parent = file.getParentFile();//3.獲取父檔案
    if (!parent.exists()) {
        if (!parent.mkdirs()) {//4.建立父檔案
            return false;
        }
    }
    try {
        success = file.createNewFile();//5.建立檔案
    } catch (IOException e) {
        success = false;
        e.printStackTrace();
    }
    return success;
}
複製程式碼

1.2.資料解析

解析成原始資料.png

/**
 * 解析原始資料,得到int陣列
 * @param path 路徑
 * @return int陣列
 */
private static int[] parseData(String path) throws IOException {
    FileReader reader = new FileReader(path);
    StringBuilder sb = new StringBuilder();
    int len = 0;
    char[] buf = new char[1024];
    while ((len = reader.read(buf)) != -1) {
        sb.append(new String(buf, 0, len));
    }
    String[] data = sb.toString().split(",");
    //String陣列轉int
    int[] ints = new int[data.length];
    for (int i = 0; i < ints.length; i++) {
        ints[i] = Integer.parseInt(data[i]);
    }
    return ints;
}
複製程式碼

2.氣泡排序與快速排序
/**
 * 氣泡排序
 * @param arr 陣列
 * @param n 個數
 */
private static void bubbleSort(int arr[], int n) {
    int i, j, t;
    // 要遍歷的次數,第i趟排序
    for (i = 1; i < n - 1; i++) {
        for (j = 0; j < n - 1; j++) {
            // 若前者大於後者,則交換
            if (arr[j] > arr[j + 1]) {
                t = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = t;
            }
        }
    }
}


/**
 * 快速排序
 *
 * @param arr   陣列
 * @param start 起點
 * @param end   重點
 */
private static void fastSort(int[] arr, int start, int end) {
    int i, j, key;
    if (start >= end) {
        return;
    }
    i = start + 1;
    j = end;
    key = arr[start];//基準位
    while (i < j) {
        while (key <= arr[j] && i < j) j--; //←--
        while (key >= arr[i] && i < j) i++; //--→
        if (i < j) {//交換
            int t = arr[j];
            arr[j] = arr[i];
            arr[i] = t;
        }
    }
    arr[start] = arr[i];//交換基準位
    arr[i] = key;
    fastSort(arr, start, j - 1);//左半
    fastSort(arr, j + 1, end);//右半
}
複製程式碼

3.資料輸出(固化到檔案)
//使用氣泡排序
// System.out.println("bubbleSort開始-----------------------");
// long start = System.currentTimeMillis();
// bubbleSort(data, data.length);
// long end = System.currentTimeMillis();
// System.out.println("bubbleSort耗時:" + (end - start) / 1000.f + "秒");

//使用快速排序
System.out.println("fastSort開始-----------------------");
long start = System.currentTimeMillis();
fastSort(data, 0, data.length-1);
long end = System.currentTimeMillis();
System.out.println("fastSort耗時:" + (end - start) / 1000.f + "秒");

String path_sort = "J:\\sf\\data\\num_sort.txt";
saveInts(data, path_sort);//將結果儲存到檔案
複製程式碼

4.簡單的散點圖繪製

用python的matplotlib,就這麼簡單
由於100W條資料太多,渲染太慢,就算渲染出來也一片糊,這裡取前3000個感受一下。

import matplotlib.pyplot as plt

def init_data():
    data_raw = open("J:\\sf\\data\\num_raw.txt").readline()
    data = data_raw.split(",")
    data = list(map(int, data))  # 將字元型列表轉為int型

    for i in range(2000, 3000):  # 檢視的資料索引範圍
        plt.scatter(i, data[i], alpha=0.6)

if __name__ == '__main__':
    init_data()
    plt.show()  # 顯示所畫的圖
複製程式碼

5.關於排序演算法

氣泡排序和使用快速

快速排序.png

氣泡排序排列:
fastSort開始-----------------------
等了一個小時都沒排出來,算了,不等了,我就點了暫停...

使用快速排序:
fastSort開始-----------------------
fastSort耗時:0.216秒 

這TM是"愚公移山""沉香劈山"的差距啊...,短短的幾行程式碼,都是智慧的結晶。
等兩個小時都排不出來和 0.216秒 完成任務,這就是演算法帶來的價值
複製程式碼

這時你也許會問:兩種排序的差距為何如此巨大,且聽下面細細分解。


二、時間複雜度(簡述):描述演算法執行時間和輸入資料之間的關係

1.一億次加法+賦值的耗時:
System.out.println("fastSort開始-----------------------");
long start = System.nanoTime();
for (int i = 0; i < 100000000; i++) {
    int a = 1 + i;
}
long end = System.nanoTime();
System.out.println("fastSort耗時:" + (end - start) + "納秒");

結果在: 6324942 納秒左右 即:6.324942 ms (1 ns = 100 0000 ms) 
複製程式碼

2.關於計算機常識

資訊.png

CPU的主頻:即CPU核心工作的時脈頻率,例如我的筆記本是2.20GHz
頻率(Hz):描述週期性迴圈訊號(包括脈衝訊號)在單位時間內所出現的脈衝數量

1GHz=1000MHz,1MHz=1000kHz,1kHz=1000Hz
2.20GHz = 2200 MHz = 2200 000 kHz = 2200 000 000 Hz 即22億Hz
即一秒鐘內CPU脈衝震盪次數為 22 億次 ,由於執行某指令需要多個時鐘週期

但由於不同指令所需的週期數是不定的,具體1s能執行多少次指令很難量化
於是一個演算法的時間複雜度應運而生,其中理想化了一個計算模型:
1.標準的簡單指令系統:運算與賦值等
2.模型機處理簡單指令的都恰好花費1個時間單位
複製程式碼

3.冒泡演算法的時間複雜度
/**
 * 氣泡排序
 * @param arr 陣列
 * @param n 個數
 */
private static void bubbleSort(int arr[], int n) {
    int i, j, t;
    // 要遍歷的次數,第i趟排序
    for (i = 1; i < n - 1; i++) {
        for (j = 0; j < n - 1; j++) {
            // 若前者大於後者,則交換
            if (arr[j] > arr[j + 1]) {
                t = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = t;
            }
        }
    }
}

其外層迴圈執行 N - 1 次。
內層迴圈最多的時候執行N次,最少的時候執行1次,平均執行 (N+1)/2 次。
所以迴圈體內的[交換邏輯]約執行:(N - 1)(N + 1) / 2 = (N^2 -1)/2 次。
按照計算複雜度的原則,去掉常數,去掉最高項係數,其複雜度為O(N^2)。

也就是說100W條資料, 最多要執行 100W*100W(即100億) 次交換邏輯 
測試了一下一次交換邏輯平均耗時500納秒左右
所以總耗時: 500 * 100億 ms = 5000 秒 = 1.3888889 時
複製程式碼

耗時計算.png


4.快速排序的分析

我在交換時放了一個count計數,最終:count = 3919355 遠比氣泡排序要少

/**
 * 快速排序
 *
 * @param arr   陣列
 * @param start 起點
 * @param end   重點
 */
private static void fastSort(int[] arr, int start, int end) {
    int i, j, key;
    if (start >= end) {
        return;
    }
    i = start + 1;
    j = end;
    key = arr[start];//基準位
    while (i < j) {
        while (key <= arr[j] && i < j) j--; //←--
        while (key >= arr[i] && i < j) i++; //--→
        if (i < j) {//交換
            int t = arr[j];
            arr[j] = arr[i];
            arr[i] = t;
        }
    }
    arr[start] = arr[i];//交換基準位
    arr[i] = key;
    fastSort(arr, start, j - 1);//左半
    fastSort(arr, j + 1, end);//右半
}

快速排序時間複雜度是:nlogn , 即平均執行 100W*log100W ≈ 20 * 100W  = 2000W 次 ,
快速排序時間複雜度的計算這裡暫時就不分析了,後面會有專題 
複製程式碼

三、空間複雜度(簡述):描述演算法消耗臨時空間和輸入資料之間的關係

暫略


四、散扯

1.資料結構與演算法分析
資料結構離不開演算法分析,結構本身是對現實中問題的抽象,演算法使之呈現  
演算法雖然可以獨立於結構存在,但資料結構可以使之絢麗多彩,變幻莫測  
複製程式碼

2.資料與結構
其實我更願意將資料和結構分開來說:
資料是應用程式存在和生存必不可少的部分,就像化學元素之於生物
而結構給了資料更好的承載體,複雜而優秀的結構有利於物種的存在與支配資源,就像人類之於酵母菌 

一個生物的DNA結構決定了它的形貌,一個生物的骨架決定了它有何優勢,如何生存。
在我眼中結構是自然的,純正的。而資料會附和與結構形成一種美妙的狀態
複製程式碼

3.演算法與分析
坦白說,我的演算法很渣,但我喜歡分析和計算,我一直覺得,演算法和計算是兩個不同的概念
計算是數學的,會依賴數學公式,特別是一些圖形相、繪製相關的計算
但演算法給人感覺很深沉或說深奧,而且條條大路通羅馬,需要分析優劣

演算法最令我失落的是:
我可以一字不落背下它,可以debug一步一步理解它,可以畫圖去演示它,
但我不想到它為何存在,第一個設計它的人是怎麼想的,這種感覺讓我很難受。
複製程式碼

4.雜談
一開始接觸佇列時感覺so easy,不就是入隊,出隊,檢視隊首嗎?
連結串列不就是一個一個接起來,這有什麼難的?你知道一件事物是什麼和你能運用它創造價值是天壤之別
當看到阻塞佇列和資訊佇列,感覺自己是多麼無知
也許可以硬記背出紅黑樹的特點,甚至實現的細節,如果不去思考一個演算法為什麼存在,
那它也許只是你腦子裡的一團乾草,沒有養分而且佔用空間。
因為演算法中的巧妙之處太多太多,一深究就StackOver(棧溢位),導致我一直避讓著演算法,但是:
複製程式碼

5.如果把一個程式比作人 :
結構是支撐人體存在的骨架
資料是附著在骨架上的血液與肉體
演算法是支配骨架和血肉的靈魂
複製程式碼

相關文章