1. 算法复杂度&&简单的排序算法
本章知识点:
- 时间复杂度
- 选择排序,冒泡排序O(n^2),插入排序
- 了解位运算——异或运算(抖机灵实现swap)和取最低位不为0的方法
- 二分法的基本应用
- 使用对数器,摆脱对OJ输入集的依赖
- Master公式:递归算法的时间复杂度计算公式
(1)认识时间复杂度
常数时间的操作
🤔❓什么是常数时间操作?
一个操作如果和输入样本的数据量没有关系,每次都是按照固定的时间内完成,就是常数操作。
在算法学习中我们关注什么?
- 在算法学习中,我们只会关注最坏的情况,就是使用O(就是big O,读作“大o”)来表示时间复杂度。
但是在工程实践中,我们可能还会关注平均的时间复杂度。 - 注意一些运算规则:如果出现多项式,只保留高阶的n,并且删掉系数。
- 如果两个算法伯仲难分,用理论上的时间复杂度难以比较他们的优劣(例如快排和归并都是O(n*logn ) 的算法),可以根据实际运行的时间来比较时间效率。这时候比较的就是所谓的“常数项时间”。
剧透一下,快排更快
Wiki
(2)三个简单的O(N^2)排序算法
选择排序
🤔❓选择排序做什么?
- 循环遍历数组,每次都通过一个个比较找到最小值。
- 找到最小值之后将最小值与数组未排序好的的第一个数swap交换。
public class Code02_BubbleSort {
public static void bubbleSort(int[] arr){
if(arr == null ||arr.length < 2){
return;
}
// "e" means "end"
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];
}
}
冒泡排序
🤔❓冒泡排序做什么?
- 循环遍历数组,将相邻两位数字比较
->如果后数字>前数字:swap交换,继续比较后面的相邻数字
->如果后数字<前数字:继续比较后面的相邻数字
public class Code02_BubbleSort {
public static void bubbleSort(int[] arr){
if(arr == null ||arr.length < 2){
return;
}
//e means end
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];
}
}
插入排序
🤔❓插入排序做什么?
理解成有一个和原数组等大的空数组,根据原数组中顺序不断向空数组中插入,每次插入都不会破坏“有序”!
一个个读入,保证有序的部分不断增加。
但是实际上操作不需要这个help辅助数组,直接在原数组上swap就好了,其实也可以理解成按顺序读入交换,保证0~0上有序,0~1上有序,0~2上有序,0~4上有序…0~n-1上有序,排序结束就好了。
public class Code03_InsertionSort {
public static void insertionSort(int[] arr){
//只有一个或者没有元素,不需要排序
if(arr == null || arr.length < 2){
return;
}
//插入排序 每次插入的是arr[i]
for(int i = 1;i < arr.length; i++){
//arr[0]不用插入,因为只有一个是有序的
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){
//这种交换方法有缺点:如果交换的两个东西存在同一块内存,就会背洗成0
//利用位运算:N^N=0, N^0=N这个性质
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
(3)二分法
在有序数组中查找一个数是否存在?(经典二分法)
- 初始化:使用L和R来表示搜索的左右边界,L表示下边界,R表示上边界。
- 只要 L<R 就一直循环每次都检查mid = L+R/2的中点位置
⚠️ L+R/2 是存在问题的,数组可能开的特别特别大,会导致L+R超过了计算机浮点可以表示的范围,导致溢出,变成负数,这里的解决办法是写成:
mid = L + ((R-L) >> 1) //二进制右移一位就是除以2
- 如果arr[mid] > find 更新上界R = mid-1
如果arr[mid] < find 更新下界R = mid+1
如果相等返回mid
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;
while (L < R){
mid = L + ((L+R) >> 1);
//原来这样子写也可以,就是要做三次条件判断不太好,其实可以写成if else的形式
// if(Sortedarr[mid] > num){
// R = mid - 1;
// }
// if(Sortedarr[mid] < num){
// L = mid + 1;
// }
if(Sortedarr[mid] == num){
return true;
}else if(Sortedarr[mid] > num){
R = mid - 1;
}else{
L = mid + 1;
}
}
// return false; 自己这样子写有问题
// 如果L=R的时候正好就有Sortarr[L] == num呢???所以不能退出循环就return false啊
return Sortedarr[L] == num;
}
}
在有序数组中,找到>=某个数最左侧的位置。
局部最小问题
⚠️ 不一定有序才能二分,这道题就是很典型的例子
(4)异或运算的性质拓展
异或运算的一些性质:
(1)0^N=N N^N=0
(2) 异或运算满足交换律和结合律
(3)可以实现不使用额外变量tmp交换两个数
走神啦:D
一个数组中有一个数字出现了奇数次,其他数出现了偶数次,请找到这个数。
使用了一会运算的性质(1) 的拓展,如果N异或自己异或了偶数次会得到0,奇数次会得到自己。
所以只要将数组中的数字全部都异或一遍,剩下的就是那个出现了奇数次的数字。
public static void printOddTimesNum1(int arr[]){
int eO = 0;
//不需要控制边界条件,只需要用值的时候可以这样写
for(int cur : arr){
eO^=cur;
}
System.out.println(eO);
}
一个数组中有两个数字出现了奇数次,其他数出现了偶数次,请找到这两个数。
这里的情况有点复杂,假设这两个出现了奇数次的数字n和m,如果把数组中的数字都异或一遍得到的结果是n^m,此路不通❕
所以考虑别的做法:
题目显然告诉我们n != m这件事情。
异或运算是位运算,因为这里是整形,所以二进制是32位的0或者1。
不相等的话就意味着着32位里面肯定有至少一位n和m是不相同的!
所以如果能做到将这一位数字都为0的数字自己异或一遍,就会得到n和m中该位为0的那一个数字
然后在用这个数字异或n^m就可以得到另外一个数字了。
这里涉及到的难点就是:
(1)怎么找到这一位不为零的数字?
(2)然后把这类数字全部提取出来呢?
第一个问题其实不难,因为异或运算已经帮我们解决了。只要n^m中不为零的位,就代表着两个数在这一位上面是不相同的。
第二个问题稍微有点难,这涉及到一个位运算的常规操作:把二进制数中最右侧的数提取出来。
这里直接举例子来看怎么做,设待提取字符串位ero:
ero: 1 0 1 0 1 1 1 1 1 1 0 0
(1)取反加一
~ero + 1 : 0 1 0 1 0 0 0 0 0 1 0 0
(2)上面两个数进行&运算
结果: 0 0 0 0 0 0 0 0 0 1 0 0
最后是1的那位就被提取出来了!确实是第一位为1的位。
代码如下:
public static void printOddTimesNum2(int[] arr){
//这个代码听课的时候就听不太懂555
//前面和num1是一样的
int e0 = 0, eOhasOne = 0;
for(int curNum : arr){
e0 ^= curNum;
}
//取反加一!再与得到那位不相同的地方
int rightOne = e0 & (~e0 + 1);
for(int cur : arr){
//再那个不同位为1的数字全部进行与运算。
if((cur & rightOne) != 0){
//因为rightOne会是一个只有一位为1,其余位为0的32位二进制数字,所以只要在它为1的位上不为1的数字只要和他&运算了,就会得到0,相同的就不会为0。
eOhasOne ^= cur;
}
}
System.out.println(eOhasOne + " " + (e0 ^ eOhasOne));
}
(4)递归行为的时间复杂度估算:master公式
T(N) = a*T(N/b) + O(N^d)
这里记一下结论吧(主打一个谁大谁做主):
-> 1) `log(b,a) > d : 复杂度为O(N^log(b,a))
-> 2)`log(b,a) = d` : 复杂度为O(N^d*logN)
-> 3)`log(b,a) < d` : 复杂度为O(N^d)