程序员代码面试指南之笔记01
一、算法数据结构基础课
第一节
一、 评估算法优劣的核心指标是什么?
(1)时间复杂度(流程决定)
(2)额外空间复杂度(流程决定)
(3)常数项时间(实现细节决定)
二、什么是时间复杂度?时间复杂度怎么估算?
(1)常数时间的操作
(2)确定算法流程的总操作数量与样本数量之间的表达式关系
(3)只看表达式最高阶项的部分
三、 何为常数时间的操作?
如果一个操作的执行时间不以具体样本量为转移,每次执行时间都是固定时间。称这样的操作为常数时间的操作。
四、 常见的常数时间的操作
(1)常见的算术运算(+、-、*、/、% 等)
(2)常见的位运算(>>、>>>、<<、|、&、^等)
(3)赋值、比较、自增、自减操作等
(4)数组寻址操作
总之,执行时间固定的操作都是常数时间的操作。
反之,执行时间不固定的操作,都不是常数时间的操作。
五、 如何确定算法流程的总操作数量与样本数量之间的表达式关系?
1,想象该算法流程所处理的数据状况,要按照最差情况来。
2,把整个流程彻底拆分为一个个基本动作,保证每个动作都是常数时间的操作。
3,如果数据量为N,看看基本动作的数量和N是什么关系。
六、 如何确定算法流程的时间复杂度?
当完成了表达式的建立,只要把最高阶项留下即可。低阶项都去掉,高阶项的系数也去掉。
记为:O(忽略掉系数的高阶项)
七、通过三个具体的例子,来实践一把时间复杂度的估算
1、选择排序
过程:
arr[0~N-1]范围上,找到最小值所在的位置,然后把最小值交换到0位置。
arr[1~N-1]范围上,找到最小值所在的位置,然后把最小值交换到1位置。
arr[2~N-1]范围上,找到最小值所在的位置,然后把最小值交换到2位置。
…
arr[N-1~N-1]范围上,找到最小值位置,然后把最小值交换到N-1位置。
估算:
很明显,如果arr长度为N,每一步常数操作的数量,如等差数列一般。
所以,总的常数操作数量 = a*(N^2) + b*N + c (a、b、c都是常数)
所以选择排序的时间复杂度为O(N^2)。
176453 从小到大排序
每个位置上的数字都是由在这个位置之后的所有数字中选择的一个最小的数字。
【1】 1 7 6 4 5 3
【2】 1 7 6 4 5 3
【3】 1 3 6 4 5 7
【4】 1 3 4 6 5 7
【5】 1 3 4 5 6 7
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,
然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。
以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
//选择排序 public class Code01_SelectionSort { public static void selectionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 0~n-1 // 1~n-1 // 2~n-1 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 上找最小值的下标 // 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); } } } // 交换arr的i和j位置上的值 private static void swap(int[] arr, int i, int j) { int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; } }
2、冒泡排序
过程:
在arr[0~N-1]范围上:
arr[0]和arr[1],谁大谁来到1位置;arr[1]和arr[2],谁大谁来到2位置…arr[N-2]和arr[N-1],谁大谁来到N-1位置
在arr[0~N-2]范围上,重复上面的过程,但最后一步是arr[N-3]和arr[N-2],谁大谁来到N-2位置
在arr[0~N-3]范围上,重复上面的过程,但最后一步是arr[N-4]和arr[N-3],谁大谁来到N-3位置
…
最后在arr[0~1]范围上,重复上面的过程,但最后一步是arr[0]和arr[1],谁大谁来到1位置
估算:
很明显,如果arr长度为N,每一步常数操作的数量,依然如等差数列一般
所以,总的常数操作数量 = a*(N^2) + b*N + c (a、b、c都是常数)
所以冒泡排序的时间复杂度为O(N^2)。
冒泡排序是一种简单的排序算法。
它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。
走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
//冒泡排序 public class Code02_BubbleSort { public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } 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); } }
3、插入排序
过程:
想让arr[0~0]上有序,这个范围只有一个数,当然是有序的。
想让arr[0~1]上有序,所以从arr[1]开始往前看,如果arr[1]<arr[0],就交换。否则什么也不做。
…
想让arr[0~i]上有序,所以从arr[i]开始往前看,arr[i]这个数不停向左移动,一直移动到左边的数字不再比自己大,停止移动。
最后一步,想让arr[0~N-1]上有序, arr[N-1]这个数不停向左移动,一直移动到左边的数字不再比自己大,停止移动。
估算时发现这个算法流程的复杂程度,会因为数据状况的不同而不同。
你发现了吗?
如果某个算法流程的复杂程度会根据数据状况的不同而不同,那么你必须要按照最差情况来估计。
很明显,在最差情况下,如果arr长度为N,插入排序的每一步常数操作的数量,还是如等差数列一般
所以,总的常数操作数量 = a*(N^2) + b*N + c (a、b、c都是常数)
所以插入排序的时间复杂度为O(N^2)。
插入排序是迭代算法,逐一获得输入数据,逐步产生有序的输出序列。
每步迭代中,算法从输入序列中取出一元素,将之插入有序序列中正确的位置。如此迭代直到全部元素有序。
//插入排序 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 做到有序 // arr[i]往前看,一直交换到合适的位置停止 // ...(<=) ? <- 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); } }
注意:
1,算法的过程,和具体的语言是无关的。
2,想分析一个算法流程的时间复杂度的前提,是对该流程非常熟悉
3,一定要确保在拆分算法流程时,拆分出来的所有行为都是常数时间的操作。这意味着你写算法时,对自己的用过的每一个系统api,都非常的熟悉。否则会影响你对时间复杂度的估算。
八、 时间复杂度的意义
抹掉了好多东西,只剩下了一个最高阶项啊…
那这个东西有什么意义呢?
时间复杂度的意义在于:
当我们要处理的样本量很大很大时,我们会发现低阶项是什么不是最重要的;每一项的系数是什么,不是最重要的。真正重要的就是最高阶项是什么。
这就是时间复杂度的意义,它是衡量算法流程的复杂程度的一种指标,该指标只与数据量有关,与过程之外的优化无关。
九、 额外空间复杂度
你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。
作为输入参数的空间,不算额外空间。
作为输出结果的空间,也不算额外空间。
因为这些都是必要的、和现实目标有关的。所以都不算。
但除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去。这部分空间就是额外空间。
如果你的流程只需要开辟有限几个变量,额外空间复杂度就是O(1)。
十、 算法流程的常数项
我们会发现,时间复杂度这个指标,是忽略低阶项和所有常数系数的。
难道同样时间复杂度的流程,在实际运行时候就一样的好吗?
当然不是。
时间复杂度只是一个很重要的指标而已。如果两个时间复杂度一样的算法,你还要去在时间上拼优劣,就进入到拼常数时间的阶段,简称拼常数项。
十一、 算法流程的常数项的比拼方式
放弃理论分析,生成随机数据直接测。
为什么不去理论分析?
不是不能纯分析,而是没必要。因为不同常数时间的操作,虽然都是固定时间,但还是有快慢之分的。
比如,位运算的常数时间原小于算术运算的常数时间,这两个运算的常数时间又远小于数组寻址的时间。
所以如果纯理论分析,往往会需要非常多的分析过程。都已经到了具体细节的程度,莫不如交给实验数据好了。
十二、 面试、比赛、刷题中,一个问题的最优解是什么意思?
一般情况下,认为解决一个问题的算法流程,在时间复杂度的指标上,一定要尽可能的低,先满足了时间复杂度最低这个指标之后,使用最少的空间的算法流程,叫这个问题的最优解。
一般说起最优解都是忽略掉常数项这个因素的,因为这个因素只决定了实现层次的优化和考虑,而和怎么解决整个问题的思想无关。
十三、 常见的时间复杂度(我们陆续都会见到的)
排名从好到差:
O(1)
O(logN)
O(N)
O(N*logN)
O(N^2) O(N^3) … O(N^K)
O(2^N) O(3^N) … O(K^N)
O(N!)
十四、 算法和数据结构学习的大脉络
1)知道怎么算的算法
2)知道怎么试的算法
我们所有的题目讲解,对于大脉络的实践贯穿始终
十五、 认识对数器
你在网上找到了某个公司的面试题,你想了好久,感觉自己会做,但是你找不到在线测试,你好心烦..
你和朋友交流面试题,你想了好久,感觉自己会做,但是你找不到在线测试,你好心烦..
你在网上做笔试,但是前几个测试用例都过了,突然一个巨大无比数据量来了,结果你的代码报错了,如此大的数据量根本看不出哪错了,你好心烦…
1,你想要测的方法a
2,实现复杂度不好但是容易实现的方法b
3,实现一个随机样本产生器
4,把方法a和方法b跑相同的随机样本,看看得到的结果是否一样
5,如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a和方法b
6,当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
public class Code01_SelectionSort { public static void selectionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 0~n-1 // 1~n-1 // 2~n-1 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 系统提供的自动排序,sort() 默认自动排序 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); } }
十六、 认识二分法
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 - 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; } }
经常见到的类型是在一个有序数组上,开展二分搜索
但有序真的是所有问题求解时使用二分的必要条件吗?
不
只要能正确构建左右两侧的淘汰逻辑,你就可以二分。
1) 在一个有序数组中,找某个数是否存在
2) 在一个有序数组中,找>=某个数最左侧的位置
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) 在一个有序数组中,找<=某个数最右侧的位置
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!"); } }