目录
本文知识点记录自左程云 算法与数据结构基础到高级教程 图片截取自视频中
常数操作
和数据量无关的操作
+ - * / 位运算 都是常数操作;
int a= arr[i]; //常数操作
int b= list.get(i) //非常数操作,list是一个链表
时间复杂度
常数操作的数量写成表达式,不要低阶项,除去最高项的系数剩下的。例如a*N²+b*N+c,写为O(N²)
对任何算法,估算其时间复杂度时,按照最差情况来估计,即耗费的时间最多是多少,取最大阶作为时间复杂度
当时间复杂度指标相同,需要比较不同算法流程常数项时间的时候,不需要再分析了,用具体的数据样本测试。因为虽然可以估算出常数操作数量,但是不同常数操作花费的时间不同,比较常数项时间就不能仅仅靠常数项的值的大小
加减法和位运算进行一次花费的时间是不同的
01_选择排序
时间复杂度:O(N^2)
//selection sort
static void selectionSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
int min = arr[i];
for (int j = i+1; j < arr.length; j++) {
if(arr[j] < min){
swap(arr[i],arr[j]);
}
}
}
}
02_冒泡排序
时间复杂度:O(N^2)
//bubble sort
static void bubbleSort(int[] arr){
for (int i = arr.length-1; i > 1; i--) {
for (int j = 0; j < i; j++) {
if(arr[j] > arr[j+1])
swap(arr[j], arr[j+1]);
}
}
}
swap() (用^运算符)
异或运算^,按位 相异为1,相同为0,可以理解为无进位相加,满足交换律、结合律,即一堆数进行^操作,与其顺序无关
为什么^满足 结果与顺序无关的性质?
根据^的无进位相加来理解:
一批数,最终结果的每一位上是0还是1,与该位上有几个1相加有关。比如,若有N个数相^,某一位上,1出现了奇数次,结果值对应的该位上,值就为1;1出现偶数次,结果值对应的该位上,值为0。显然,结果与顺序无关。
若两个数存储在两个不同的内存区域中,则可以这样交换
int a=10,b=20;
void swap(int a, int b){ //若传入参数是数组元素,则两数下标不能相同
a= a^b;
b= a^b;
a= a^b;
}
^的应用
//在一个数组中,只有两个数出现了奇数次,其他数都出现了偶数次,返回这两个数
class Solution {
//从n右边往左,找第一个值为1的位,在该处,a与b的值是不同的,根据这一位上的值可以把a和b从nums里面分到两个区域
int GetRightOne(int n){
return n^(n&(n-1));
}
public int[] singleNumbers(int[] nums) {
int[] res = new int[2];
res[0] = 0;
res[1] = 0;
int tmp = 0;
for(int i = 0; i < nums.length; ++i){
tmp ^= nums[i]; //temp=a^b(a b即是要返回的两数)
}
int rightone = GetRightOne(tmp);
for(int i = 0; i < nums.length; ++i){
if((nums[i] & rightone) == rightone){
res[0] ^= nums[i];
}else{
res[1] ^= nums[i];
}
}
return res;
}
}
03_插入排序
时间复杂度:O(N^2)
//insertion sort, method b
static void insertionSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
for (int k = i; k > 0 && (arr[k] < arr[k-1]); --k) {
swap(arr[k], arr[k-1]);
}
}
}
二分
二分与数组有序无序无关,不是只有有序才能用二分
- 在一个有序数组中,找某个数是否存在
O(logN) //时间复杂度,logN默认以2为底
//每次对半分,找比较中间的数和要找的数的大小关系,最坏的情况:分到不能再分才找到,即进行了logN次比较
- 在一个有序数组中,找 >=某个数最左侧的位置
- 局部最小值
给一个无序且互不相等的数组,求其局部最小值。局部最小值定义为:若a[0]<a[1],则0位置为局部最小;若a[n-2]>a[n-1],n-1位置局部最小;若a[i]<a[i-1] && a[i]<a[i+1] ,i位置为局部最小。
求L和R的中点(L和R是数组索引):
int mid = L + (R-L)>>1;
这样写不容易越界(当L和R非常大时,L+R可能溢出);
(R-L)>>1 比(R-L)/2 更快。
对数器
- 有一个想测试的方法a(复杂度更好)
- 实现复杂度不好,但是容易实现的方法b;
- 实现一个随机样本产生器
- 方法a和方法b跑相同的随机样本,看看结果是否一致
- 若有一个样本使得比对结果不一致,打印样本进行人工干预,更改方法a或方法b
- 当样本数量很多时,比对测试依然正确,可以确定方法a已经正确
为什么要有对数器?
各种OJ也是人工输入的测试用例,不排除你的算法有问题时,测试用例没有测出来(没包含进所有的情况),所以自己设计对数器,进行人工干预,这是最稳妥的测试方法,不会出现上面的错误。
对数器的实现
import java.util.Arrays;
public class Test {
//交换两数
static void swap(int a, int b){
int tmp = a;
a = b;
b = tmp;
}
//若确定a和b不指向同一块内存,则swap可以这样实现
// static void swap(int a, int b){
// a = a ^ b;
// b = a ^ b;
// a = a ^ b;
// }
//打印数组
static void print(int[] arr){
for(int a : arr)
System.out.print(a+" ");
}
//bubble sort
static void bubbleSort(int[] arr){
for (int i = arr.length-1; i > 1; i--) {
for (int j = 0; j < i; j++) {
if(arr[j] > arr[j+1])
swap(arr[j], arr[j+1]);
}
}
}
//selection sort, method a
static void selectionSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
int min = arr[i];
for (int j = i+1; j < arr.length; j++) {
if(arr[j] < min){
swap(arr[i],arr[j]);
}
}
}
}
//insertion sort, method b
static void insertionSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
for (int k = i; k > 0 && (arr[k] < arr[k-1]); --k) {
swap(arr[k], arr[k-1]);
}
}
}
//generate random array
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+1)*Math.random());
}
return arr;
}
public static void main(String[] args) {
//logarithmic detector
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean sign = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = Arrays.copyOf(arr1,arr1.length);
selectionSort(arr1);
bubbleSort(arr2);
if(!Arrays.equals(arr1,arr2)){
sign = false;
break;
}
}
System.out.println(sign ? 666 : "gg");
if((sign ? 666 : "gg") == "gg"){
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = Arrays.copyOf(arr1,arr1.length);
print(arr1);
selectionSort(arr1);
print(arr1);
bubbleSort(arr2);
print(arr2);
}
}
}
递归行为和递归行为时间复杂度的估算
递归行为可以理解为多叉树的遍历,每一个节点要计算值,必须依赖当前节点的子节点。由于这种依赖关系,在没执行到叶子节点前,就相当于不断把节点压入栈中(这实际上是一个递推的过程);到了叶子节点,不再依赖其他节点就可以返回当前值了,于是相当于弹栈(相当于回溯过程)。
递归时间复杂度的估算
master公式(适用于子问题等规模的递归时间复杂度的求解)
T(N) = a*T(N/b) + O(N^d)
l o g b a < d → O ( N d ) log_ba<d→O(N^d ) logba<d→O(Nd)
l o g b a > d → O ( N x ) . . . . . . x = l o g b a log_ba>d→O(N^x)......x=log_ba logba>d→O(Nx)......x=logba
l o g b a = d → O ( N d ∗ l o g N ) log_ba=d→O(N^d*logN) logba=d→O(Nd∗logN)