程序=算法+数据结构,既然算法这么重要,每个人写出来的算法又不一样,那么怎么算是一个好的算法呢?
你比如数组的寻址操作,这就是一个常数项时间操作,每次执行时间都是固定时间,与数据量的大小无关,这样的操作就属于常数项时间操作。、
这里再举一个例子!你们不是Java里面经常用到LinkedList吗?这个在底层属于双向链表,如果你要list.get(5),那么并不会像数组寻址那样直接计算偏移量,而是一个一个地去遍历,到了合适的位置才取值,这个就不是常数项时间。
了解常数时间有什么用?时间复杂度就是来衡量你这个算法中基本常数操作执行的次数,看到底和数据量大小之间存在什么样的关系?很难理解吧?我们现在用选择排序(思想:每次选择出数组中最小的数值,然后和最小位置的数字进行交换,然后最小位置+1,直到选择排序完毕)来介绍时间复杂度。这里视频中讲了选择排序的常数操作num=XN^2 + YN + Z,想一下为什么?时间复杂度为o(N^2),只需要最高次幂。
当完成了表达式的建立后,只要把最高阶项留下即可。低阶项都去掉,高阶项的系数也要去掉,记为o(忽略掉系数的高阶项)
当我们要处理的样本量很大很大的时候,我们会发现低阶项什么都不是最重要的了,每一项的系数是什么也不是最重要的,真正重要的是最高阶项是什么。时间复杂度是衡量算法流程复杂程度的一种指标,这个指标只与数据量有关,与过程之外的优化无关(一会再讲这一句)。
// 到了这里我们就把选择排序、冒泡排序、插入排序都讲了吧!这三个的时间复杂度都是o(n^2)
class Test {
// 选择排序思想:每次选择出数组中最小的数值,然后和最小位置的数字进行交换,然后最小位置+1,直到选择排序完毕
public static void selectSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
// 这里用i来表示下标
for (int i = 0; i < arr.length - 1; i++) {
int min_index = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[min_index] > arr[j]) {
min_index = j;
}
// 交换一次后我现在是当前最小的,我还要看后面有没有更小的,所以继续循环
swap(arr, i, min_index);
}
}
}
// 冒泡排序思想:0-1谁大谁往后,1-2谁大谁往后,....,4-5谁大谁往后,这样一轮后,最大的值在最后面;第二轮开始....第三轮开始....,最后排序完毕。
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
// 0 ~ N-1
// 0 ~ N-2
// 0 ~ N-3
// 0 ~ 1
for (int e = arr.length - 1; e > 0; e--) {
// 0 1
// 1 2
// 2 3
// e-1 e
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
// 插入排序思想:先让0~1有序,再让0~2有序,.....,最后再让0~N-1有序
public static void insertSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
for(int i = 1;i < arr.length;i++){
for(int j = i - 1;j >= 0 && arr[j] > arr[j+1];j--){
swap(arr,j,j+1);
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp;
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void printArr(int[] arr) {
for (int e : arr) {
System.out.print(e + " ");
}
System.out.println();
}
public static void main(String[] args) {
}
}
o(1) < o(logN) < o(N) < o(N*logN) < o(N^2) < … < o(N^k) < o(2^n) < … < o(k^n) < o(n!)
你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。作为输入参数的空间,不算额外空间,作为输出结果的空间,也不算额外空间。因为这些都是必要的,和现实目标有关的,所以都不算。但是除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去,这部分空间就是额外空间,如果你的流程只需要开辟有限几个变量,额外空间复杂度就是o(1)。
那么我们就可以判断出来什么是最优解?最优解就是在在时间复杂度的指标上要尽可能得低,先满足了时间复杂度最低这个指标之后,使用最少的空间的算法流程,叫做这个问题的最优解。一般来说最优解都是忽略掉常数项这个因素的,因为这个因素只决定了实现层次的优化和考虑,而和怎么解决整个问题的思想无关。
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);
}
}
下面用一些题来认识二分。
// 在一个有序数组中,找某个数是否存在
package class01;
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
// X*2+1可以表示为( X<<2 | 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;
}
}
// 在一个有序数组中,找>=某个数最左侧的位置
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!");
}
}
// 在一个有序数组中,找<=某个数最右侧的位置
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!");
}
}
// 局部最小值问题
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。
能长时间记住的概率接近0%,所以,异或运算就记成无进位相加(无进位是什么意思?就是二进制去相加,即便两者都是1,相加结果是2,不要进位,也就是得到0且不要进位)。
0^N == N、N^N == 0。
异或运算满足交换律和结合律。
上面的两个性质用无进位相加来理解就非常得容易。
public class Test {
public static void main(String[] args) {
// 如何不需要额外的变量即可实现两个数字的交换,但是特别注意,只有两个变量用的不是同一个内存的时候才可以这样干!
int a = 10;
int b = 4;
a = a ^ b;
b = a ^ b;
a = a ^ b;
// b = a ^ b ^ b = a ^ 0 = a; 运用交换律和结合律
// a = a ^ b = a ^ b ^ a ^ b ^ b = a ^ a ^ b ^ b ^ b = b; 运用交换律和结合律
System.out.println("a=" + a + ",b=" + b);
}
}
public class Test {
// 一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这个数字?
public static void printOddTimesNum1(int[] arr){
int eor = arr[0];
for(int i = 1; i < arr.length;i++){
eor ^= arr[i];
}
System.out.println(eor);
}
public static void main(String[] args) {
int[] arr = new int[]{1,1,2,2,3,3,4};
printOddTimesNum1(arr);
}
}
public class Test {
// 怎么把一个整数,提取出最右侧的1来
public static int func(int number){
// 00000011 01010000
// 11111100 10101111 (取反)
// 11111100 10110000 (加1)
// 00000000 00010000 (与)
return number & ((~number) + 1);
}
public static void main(String[] args) {
int num = 234;
System.out.println(func(num));
}
}
public class Test {
// 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[1] = 111100011110000
// rightOne= 000000000010000
if ((arr[i] & rightOne) != 0) {
onlyOne ^= arr[i];
}
}
System.out.println(onlyOne + " " + (eor ^ onlyOne));
}
public static void main(String[] args) {
}
}
public class Test {
// 看一个数字的二进制样式中有几个1
public static int func(int num){
int count = 0;
while(num != 0){
int rightOne = num & ((~num) + 1);
count++;
num ^= rightOne;
}
return count;
}
public static void main(String[] args) {
int num = 234;
System.out.println(func(num));
}
}