1. 程式人生 > >《演算法筆記一》複雜度、排序、二分、異或

《演算法筆記一》複雜度、排序、二分、異或

[TOC] # 時間複雜度、空間複雜度、排序、異或運算 ## 時間複雜度 - 常數時間操作: 1. 算數運算:+ - * / 2. 位運算:>>(帶符號右移動)、 >>>(不帶符號右移動) 、 <<、 | 、& 、^ ==帶符號就是最高位補符號位,不帶符號就是最高位補0== 3. 賦值操作:比較,自增,自減操作 4. 陣列定址等 > 總之,執行時間固定的操作都是常數時間的操作。反之執行時間不固定的操作,都不是常數時間的操作 - 通過基本動作的常數時間,推導時間複雜度 > 對於雙層迴圈來說,n*(常數)+ (n-1)*(常數)+ ... + 2*(常數) + 1*(常數) => 推匯出 ```math y = an^2 + bn + c ``` ==忽略掉低階項,忽略掉常數項,忽略掉高階項的係數,得到時間複雜度為n^2== ### 排序操作 #### 選擇排序 ```Java package class01; import java.util.Arrays; public class Code01_SelectionSort { public static void selectionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 0 ~ N-1 // 1~n-1 // 2 for (int i = 0; i < arr.length - 1; i++) { // i ~ N-1 // 最小值在哪個位置上 i~n-1 int minIndex = i; for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下標 minIndex = arr[j] < arr[minIndex] ? j : minIndex; } swap(arr, i, minIndex); } } public static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } // for test public static void comparator(int[] arr) { Arrays.sort(arr); } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { // Math.random() [0,1) // Math.random() * N [0,N) // (int)(Math.random() * N) [0, N-1] int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { // [-? , +?] arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test 對數器 public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); selectionSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { succeed = false; printArray(arr1); printArray(arr2); break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); selectionSort(arr); printArray(arr); } } ``` #### 氣泡排序 ```Java package class01; import java.util.Arrays; public class Code02_BubbleSort { public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 0 ~ N-1 // 0 ~ N-2 // 0 ~ N-3 for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e for (int i = 0; i < e; i++) { if (arr[i] > arr[i + 1]) { swap(arr, i, i + 1); } } } } // 交換arr的i和j位置上的值 public static void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } // for test public static void comparator(int[] arr) { Arrays.sort(arr); } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); bubbleSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); bubbleSort(arr); printArray(arr); } } ``` #### 插入排序 ```Java package class01; import java.util.Arrays; public class Code03_InsertionSort { public static void insertionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 0~0 有序的 // 0~i 想有序 for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序 for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } } } // i和j是一個位置的話,會出錯 public static void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } // for test public static void comparator(int[] arr) { Arrays.sort(arr); } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { // Math.random() -> [0,1) 所有的小數,等概率返回一個 // Math.random() * N -> [0,N) 所有小數,等概率返回一個 // (int)(Math.random() * N) -> [0,N-1] 所有的整數,等概率返回一個 int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; // 長度隨機 for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; // 隨機陣列的長度0~100 int maxValue = 100;// 值:-100~100 boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); insertionSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { // 列印arr1 // 列印arr2 succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); insertionSort(arr); printArray(arr); } } ``` ==插入排序和前面兩種排序的不同是在於,插入排序跟陣列初始順序有關,在初始有序的情況下,有可能時間複雜度為O(N),有可能為O(N ^2),但是我們估計時間複雜度要按照最差的情況來估計,所以插入排序的時間複雜度仍然O(N ^2)== ## 空間複雜度 申請有限幾個變數,和樣本量n沒關係,就是空間複雜度O(1),如果要開闢一個空間陣列和樣本量n是一樣大,用來支援我們的演算法流程那麼O(N)。反之使用者就是要實現陣列拷貝,我們開闢一個新的n大小陣列用來支撐使用者的需求,那麼仍然是O(1) ## 常數項時間複雜度 如果兩個相同時間複雜度的演算法要比較效能,這個時候需要比較單個常數項時間,對能力要求較高,沒有意義,不如樣本量試驗實際測試來比較 ## 演算法最優解 我們認為最優解的考慮順序是,先滿足時間複雜度指標,再去使用較少的空間。一般來說,演算法題,ACM等不會卡常數項時間 ## 常見時間複雜度 > 依次從好到壞 O(1) -> O(logN) -> O(N) -> O(N*logN) -> O(N^2) -> O(N^3) ... -> O(N!) ## 演算法和資料結構脈絡 1. 知道怎麼算的演算法 2. 知道怎麼試的演算法(遞迴) ## 認識對數器 1. 準備你想要測試的方法a 2. 實現一個複雜度不好,但是容易實現的方法b 3. 實現一個隨機樣本產生器 4. 把方法a和方法b跑相同的隨機樣本,看看得到的結果是否一樣 5. 如果有一個隨機樣本使得對比結果不一致,列印樣本進行人工干預,改對方法a和方法b 6. 當樣本數量很多,測試對比依然正確,可以確定方法a已經正確 ## 認識二分法 1. 在一個有序陣列中,找某個數是否存在 > 二分查詢值,基於有序陣列,演算法複雜度為二分了多少次,O(log2N)可以寫成O(logN) > 123579 ```Java package class01; import java.util.Arrays; public class Code04_BSExist { public static boolean exist(int[] sortedArr, int num) { if (sortedArr == null || sortedArr.length == 0) { return false; } int L = 0; int R = sortedArr.length - 1; int mid = 0; // L..R while (L < R) { // mid = (L+R) / 2; // L 10億 R 18億 // mid = L + (R - L) / 2 // N / 2 N >> 1 mid = L + ((R - L) >> 1); // mid = (L + R) / 2 if (sortedArr[mid] == num) { return true; } else if (sortedArr[mid] > num) { R = mid - 1; } else { L = mid + 1; } } return sortedArr[L] == num; } // for test public static boolean test(int[] sortedArr, int num) { for(int cur : sortedArr) { if(cur == num) { return true; } } return false; } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } public static void main(String[] args) { int testTime = 500000; int maxSize = 10; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr = generateRandomArray(maxSize, maxValue); Arrays.sort(arr); int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); if (test(arr, value) != exist(arr, value)) { succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); } } ``` 2. 在一個有序陣列中,找>=某個數最左側的位置 > 122222333578888999999 找大於等於2最左側的位置 ```Java package class01; import java.util.Arrays; public class Code05_BSNearLeft { // 在arr上,找滿足>=value的最左位置 public static int nearestIndex(int[] arr, int value) { int L = 0; int R = arr.length - 1; int index = -1; // 記錄最左的對號 while (L <= R) { int mid = L + ((R - L) >> 1); if (arr[mid] >= value) { index = mid; R = mid - 1; } else { L = mid + 1; } } return index; } // for test public static int test(int[] arr, int value) { for (int i = 0; i < arr.length; i++) { if (arr[i] >= value) { return i; } } return -1; } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } public static void main(String[] args) { int testTime = 500000; int maxSize = 10; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr = generateRandomArray(maxSize, maxValue); Arrays.sort(arr); int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); if (test(arr, value) != nearestIndex(arr, value)) { printArray(arr); System.out.println(value); System.out.println(test(arr, value)); System.out.println(nearestIndex(arr, value)); succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); } } ``` 3. 在一個有序陣列中,找<=某個數最右側的位置 ```Java package class01; import java.util.Arrays; public class Code05_BSNearRight { // 在arr上,找滿足<=value的最右位置 public static int nearestIndex(int[] arr, int value) { int L = 0; int R = arr.length - 1; int index = -1; // 記錄最右的對號 while (L <= R) { int mid = L + ((R - L) >> 1); if (arr[mid] <= value) { index = mid; L = mid + 1; } else { R = mid - 1; } } return index; } // for test public static int test(int[] arr, int value) { for (int i = arr.length - 1; i >= 0; i--) { if (arr[i] <= value) { return i; } } return -1; } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } public static void main(String[] args) { int testTime = 500000; int maxSize = 10; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr = generateRandomArray(maxSize, maxValue); Arrays.sort(arr); int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); if (test(arr, value) != nearestIndex(arr, value)) { printArray(arr); System.out.println(value); System.out.println(test(arr, value)); System.out.println(nearestIndex(arr, value)); succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); } } ``` 4. 區域性最小值問題 > 無序陣列,任意兩個相鄰的數不相等,返回一個區域性最小值 ```Java package class01; public class Code06_BSAwesome { public static int getLessIndex(int[] arr) { if (arr == null || arr.length == 0) { return -1; // no exist } if (arr.length == 1 || arr[0] < arr[1]) { return 0; } if (arr[arr.length - 1] < arr[arr.length - 2]) { return arr.length - 1; } int left = 1; int right = arr.length - 2; int mid = 0; while (left < right) { mid = (left + right) / 2; if (arr[mid] > arr[mid - 1]) { right = mid - 1; } else if (arr[mid] > arr[mid + 1]) { left = mid + 1; } else { return mid; } } return left; } } ``` ## 認識異或運算 異或運算:相同為0,不同為1 同或運算:相同為1, 不同為0,不掌握 ==上述特別不容易記住,異或運算就記成無進位相加:比如十進位制6異或7,就理解為110和111按位不進位相加,得到001== 1. 所以 0^N = N , N^N = 0 2. 異或運算滿足交換律和結合律,所以A異或B異或C = A異或(B異或C) = (A異或C)異或B 題目一:如何不用額外變數就交換兩個數 ```shell a = x b = y兩個數交換位置 a = a ^ b # 第一步操作,此時 a = x^y , b=y b = a ^ b # 第二步操作,此時 a = x^y , b = x^y^y => b = x^0 => b = x a = a ^ b # 第三步操作,此時 a = x^y^x, b = x, a=> x^x^y => a=y 三步操作,實現交換ab的值 ``` ```Java package class01; public class Test { public static void main(String[] args) { int a = 6; int b = 6; a = a ^ b; b = a ^ b; a = a ^ b; System.out.println(a); System.out.println(b); int[] arr = {3,1,100}; System.out.println(arr[0]); System.out.println(arr[2]); swap(arr, 0, 0); System.out.println(arr[0]); System.out.println(arr[2]); } public static void swap (int[] arr, int i, int j) { // arr[0] = arr[0] ^ arr[0]; arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } } ``` ==注意,如果a和b指向同一塊記憶體,改方法不可行== 題目二:一個數組中有一種數出現了奇數次,其他數都出現了偶數次,怎麼找到並列印這種數 > [2,2,1,3,2,3,2,1,1] 陣列中存在四個2,兩個3,三個1,定義一個常量等於0,分別對該陣列中的數遍歷一遍進行異或,最後,該變數等於多少,那麼奇數的值就是多少。因為異或運算滿足交換和結合律 題目三:怎麼把一個int型別的數,提取出最右側的1來 > n與上(n取反加1)即可 => N & ( (~N)+1 ) 題目四:一個數組中有兩種不相等的數出現了奇數次,其他數出現了偶數次,怎麼找到並列印這兩種數 > 定義一個常量eor = 0,分別對該陣列每個數異或,最終結果為a異或b,其中a和b就是這兩個奇數,由於a!=b所以a異或b不等於0,即eor的值某一位上一定為1(有可能不止一個1隨便選一個例如第八位),用該位做標記對原有陣列的數進行分類,那麼a和b由於第八位不相同一定被分開,再定義常量eor' = 0分別對第八位為0的數異或,那麼得到的值,就是a和b其中一個,由於之前eor = a異或b,那麼在用eor和eor'異或,就是另外一個值。一般來說,隨便找一個1我們就找最右側的那個1,如題目三 ```Java package class01; public class Code07_EvenTimesOddTimes { // arr中,只有一種數,出現奇數次 public static void printOddTimesNum1(int[] arr) { int eor = 0; for (int i = 0; i < arr.length; i++) { eor ^= arr[i]; } System.out.println(eor); } // arr中,有兩種數,出現奇數次 public static void printOddTimesNum2(int[] arr) { int eor = 0; for (int i = 0; i < arr.length; i++) { eor ^= arr[i]; } // eor = a ^ b // eor != 0 // eor必然有一個位置上是1 // 0110010000 // 0000010000 int rightOne = eor & (~eor + 1); // 提取出最右的1 int onlyOne = 0; // eor' for (int i = 0 ; i < arr.length;i++) { // arr[i] = 111100011110000 // rightOne= 000000000010000 if ((arr[i] & rightOne) != 0) { onlyOne ^= arr[i]; } } System.out.println(onlyOne + " " + (eor ^ onlyOne)); } public static int bit1counts(int N) { int count = 0; // 011011010000 // 000000010000 1 // 011011000000 // while(N != 0) { int rightOne = N & ((~N) + 1); count++; N ^= rightOne; // N -= rightOne } return count; } public static void main(String[] args) { int a = 5; int b = 7; a = a ^ b; b = a ^ b; a = a ^ b; System.out.println(a); System.out.println(b); int[] arr1 = { 3, 3, 2, 3, 1, 1, 1, 3, 1, 1, 1 }; printOddTimesNum1(arr1); int[] arr2 = { 4, 3, 4, 2, 2, 2, 4, 1, 1, 1, 3, 3, 1, 1, 1, 4, 2, 2 }; printOddTimesNum2(arr2);