一、认识时间复杂度
常数时间的操作:一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(读作big O)来表示。
具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作, 进而总结出常数操作数量的表达式。
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那 么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行 时间,也就是“常数项时间”。
二、选择排序
时间复杂度O(N^2),额外空间复杂度O(1)
/**
* 选择排序 时间复杂度O(n^2)
* 从数组第一个元素开始遍历,后一个和前一个元素比较大小,将最小的元素的下标赋值给 minIndex
* 然后执行swap方法,替换下标为i和minIndex元素的位置,这样每次选择出来的最小的数组就排在前面了
* 最外层for循环结束以后 整个数组就是从小到大排序的了
*/
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
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;
//交换方法二 异或运算
//arr[i] = arr[i] ^ arr[minIndex];
//arr[minIndex] = arr[i] ^ arr[minIndex];
//arr[i] = arr[i] ^ arr[minIndex];
}
三、冒泡排序
时间复杂度O(N^2),额外空间复杂度O(1)
/**
* 冒泡排序 时间复杂度O(n^2)
* 最外层for循环遍历 n 次,内层for循环先从第一个元素开始和下一个元素比较
* 然后从第二个元素开始和第一个元素比较
* 一直到倒数第二个元素和倒数第一个元素比较结束
* 这样第一次循环就把最大的放在了最后面
* 继续循环,倒数第二大的放在倒数第二个位置
* 以此类推.........
* 最外层for循环结束以后 整个数组就是从小到大排序的了
*/
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
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];
}
四、插入排序
时间复杂度O(N^2),额外空间复杂度O(1) 算法流程按照最差情况来估计时间复杂度
/**
* 插入排序 时间复杂度(最坏情况) O(n^2) 最好情况O(n)
* 最外层for循环从第二个元素开始直到最后一个元素
* 最内层for循环从第一个元素开始,如果j不越界 而且 j下标元素比 j+1 元素大
* 那么就交换这两个元素的位置
*/
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
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) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
五、二分法
1)在一个有序数组中,找某个数是否存在
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;
while (L < R) {
mid = L + ((R - L) >> 1);
if (sortedArr[mid] == num) {
return true;
} else if (sortedArr[mid] > num) {
R = mid - 1;
} else {
L = mid + 1;
}
}
return sortedArr[L] == num;
}
2)在一个有序数组中,找>=某个数最左侧的位置
// 在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;
}
3)局部最小值问题
六、异或运算的性质与扩展
- 0^N == N N^N == 0
- 异或运算满足交换律和结合率
- 不用额外变量交换两个数
- 一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到这一个数
- 一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到这两个数
题目举例:
- 一个数组 1种数字是出现奇数次 其余数字是出现偶数次 筛选出这一种数字是什么
int eO = 0;
//遍历数组 做异或运算 偶数次的数字异或都是0 只有奇数次的数字异或后就是这个数字
for (int cur : arr) {
eO ^= cur;
}
System.out.println(eO);
- 一个数组 2种数字是出现奇数次 其余数字是出现偶数次 筛选出这两种数字是什么
int eO = 0, eOhasOne = 0;
//假设a和b是两个出现奇数次的数字
//先把每个数 都异或起来 最后 e0 = a^b (a 异或 b)
for (int curNum : arr) {
eO ^= curNum;
}
//因为 a ≠ b ,所以 e0 肯定有一位是 1 ,异或相异为 1
//那么 取出这一位 1
// ~ 取反 +1 再和 e0 与运算
int rightOne = eO & (~eO + 1);
//再遍历数组 所有数字 只要第几位不是 0 那么最后 eOhasOne 就是 a或者b
for (int cur : arr) {
if ((cur & rightOne) != 0) {
eOhasOne ^= cur;
}
}
// eO ^ eOhasOne 就是剩下那个数字了
System.out.println(eOhasOne + " " + (eO ^ eOhasOne));
七、对数器的概念和使用
- 有一个你想要测的方法a
- 实现复杂度不好但是容易实现的方法b
- 实现一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
- 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者 方法b
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
八、剖析递归行为和递归行为时间复杂度的估算
用递归方法找一个数组中的最大值,系统上到底是怎么做的?
master公式的使用 T(N) = a*T(N/b) + O(N^d)
- log(b,a) > d -> 复杂度为O(N^log(b,a))
- log(b,a) = d -> 复杂度为O(N^d * logN)
- log(b,a) < d -> 复杂度为O(N^d)
补充阅读:www.gocalf.com/blog/algorithm-complexity-and-master- theorem.html