(HP)算法(数据结构)

时间/空间复杂度

1,时间复杂度

数组在内存的空间是连续的,链表的空间是不连续的;所以数组arr[i]的时间复杂度是O(1),想要找第n个数据,直接根据偏移量一下子找到了,不需要遍历;而链表由于不是连续的存储空间,想要找到第n个元素只能一个一个去遍历,跟链表中数据的体量有关系,所以时间复杂度是O(n)

加减乘除,位运算都是一个常数操作,时间复杂度都是O(1);

2,如何计算时间复杂度

根据数据体量得出一个(最差)算术操作的次数方程式,取这个方程式的最高阶项,并去掉它的系数,就是它的时间复杂度。

例如,将一个无序的数组排列成有序的数组,需要查询+比较+替换的总次数 = aN^{^{2}}+bN+c

所以 这个算法的时间复杂度=O(N^{^{^{2}}})

3,如何选取时间复杂度

选取最高阶项简单的算法,如果两个最高阶项相同,不能直接通过比较常数项大小做取舍,因为算法中还包括其他的加减乘除不相同,只能实际去运行代码去测试哪个时间更短。

4,常见算法时间复杂度

选择排序:后面的数据与每一轮的第一个数据挨个比较。 O(N^{2})

冒泡排序:相邻的数据互相比较。 O(N^{2})

插入排序:后一个数据和所有前面的数据比较。O(N^{2})

插入排序最优时间复杂度:O(N),所以插入比选择和冒泡速度快

二分法:O(log{_2{}}^{n}),即简写 O({log_{}}^{n})

二分法不一定使用在有序数组中,无序数组也可以二分,比如寻找小部分区间。

递归算法:时间复杂度需要通过master公式计算

// 常见递归格式
process()
function process(){
  const a = process();
  return a
}

master公式:指递归公式中,所有子递归的规模相等的通用公式。

 T(N) = a T({\frac{N}{b}} )+ O(N^{d})

如果{log_{b}}^{a}< d:时间复杂度=O(N^{d})

如果{log_{b}}^{a}> d:时间复杂度=O(N^{log_{b}^{a}})

如果{log_{b}}^{a}== d:时间复杂度=O(N^{d}*log_{}^{N})

比如二分法递归: T(N) = 2T(\frac{N}{2})+O(1)

 归并排序:将一个数组排序,先二分法将左侧右侧分别排序,然后创建辅助空间➕两个指针,左边比右边小就将左边push到辅助空间,同时指针后移,以此类推。

由于归并排序使用到递归方法,其master公式得出 {log_{b}}^{a}== d,所以时间复杂度 = O(N^{d}*log_{}^{N}) 

快速排序1.0:将一个数组的最后一个数作为中间值,大于该值划分到右侧,小于该值划分到左侧。然后左右再分别递归划分。 

快速排序2.0:将一个数组的最后一个数作为中间值,大于该值划分到右侧,小于该值划分到左侧,等于该值划分到中间。然后左右再分别递归划分。(荷兰🇳🇱国旗法)

快速排序1.0和2.0的时间复杂度都是 O(N^{2})

快速排序3.0:随机选择一个数字做划分,由于这个数可能刚好是最大值,最小值,或者中位数,所以(二分法递归)快速排序的公式有很多种可能,比如:
T(N) = 2T({\frac{N}{2}})+O(N)
T(N) = {\frac{1}{3}}T({\frac{N}{3}})+{\frac{2}{3}}T({\frac{2N}{3}})+O(N)
.......

将所有公式等权重相加,得到时间复杂度:O(N*log^{N})

堆排序:将数组排序成一个大根堆,然后将最后一个值和第一个值做交换,然后堆长度(heapsize)减一。
将剩下的堆排序成一个大根堆,将最后一个值和第一个值做交换,然后堆长度(heapsize)减一。
周而复始,直到heapsize=0。

堆排序时间复杂度是:O(N*log^{N})

代码:

计数排序:遍历数组,准备一个容器,统计每个数据出现的次数,然后将数据push到新数组,时间复杂度O(N),局限性:仅限小数据量。

基数排序:计数排序的升级版。因为计数排序统计容器(桶)的个数不确定,根据数据量而定。而基数排序桶的个数只与所排序数据的进制(2进制2个桶,10进制10个桶)有关。

4,空间复杂度

计算过程中,额外开辟的空间数。

比如选择排序,将一个无序的数组排列成有序的数组:

for(let i=0;i<len;i++){
  let min = 0;
  for(let j=i;j<len;j++){
     min = min>arr[j]?arr[j]:min
  }
}

在这个计算过程中,产生了一个额外的i,j,min变量,是有限个常数量的空间,空间复杂度=O(1) 

运算符号

1,异或^

位运算,相同为0,不同为1;1001^0111 = 1110;异或运算也叫相加不进位运算。

异或的使用场景1:可以作为变量交换而不开辟一个新空间的操作

比如,a与b交换值:

a = a^b;
b = a^b;
a = a^b;

解析:

a = 23;b = 6;
a = a ^ b;  // 23 ^ 6
b = a ^ b; // 23 ^ 6 ^ 6 = 23
a = a ^ b; // 23 ^ 6 ^ 23 = 23 ^ 23 ^ 6 = 6

使用条件:

只允许交换不同内存空间的两个数,不允许交换同一个内存空间的一个数。

正确使用:a = arr[1],b= arr[2]

错误使用:a = arr[1], b = arr[1];结果会导致该空间变量抹成0。

异或的使用场景2:可以从数组中找出唯一一个出现奇数次的数,时间复杂度O(n),空间复杂度O(1)

const arr = [1,1,2,2,3]
let res = 0;
for(let i=0;i<arr.length;i++){
 res ^ = arr[i];
}
return res;

异或的使用场景3: 可以从数组中找出两个出现奇数次的数,时间复杂度O(n),空间复杂度O(1)

 // 假如结果是res_one和res_two
const arr = [1,2,2,3];
let two = 0;
let res_one = 0;

// 先得到res_one ^ res_two
for(let i = 0;i<arr.length;i++){
    two ^ = arr[i];  // two = res_one ^ res_two
}

// 再得到res_one ^ res_two最右侧的1
let only = two & (~two +1);

// 最右侧的1和数组所有的值与,一定能得到两个结果中的一个
for(let i = 0;i<arr.length;i++){
 if( (only & arr[i]) === only ){
     res_one ^ = arr[i];
 }
}

// 这是另外一个结果。
let res_two = two ^ res_one;

2,取反 ~

~10010  = 01101

3,与 &

两个值都是真结果为真,其余都会假。1 & 1 = 1,1 & 0 = 0, 0 & 0=0

使用场景:取一个数最右侧的1,比如10110100最右侧的1是100110100

let. a = 10010;
// 取a最右侧的1;
let b = a & (~a + 1); // 10010 & (01101 + 1) = 10010 & 01110 = 00010

4,移位>>

二进制位运算

 右移:表示除2

左移:表示乘2求数组中点防止溢出:

const mid = (arr[j] +  arr[i] ) / 2
//可以修改成: 
const mid = arr[i] + ( (arr[j]- arr[i])>>1 )

测试方法

1,对数器

 解决一个问题有两种方法,方法a和方法b,其中方法a更优秀,想要测试一下是否编写正确,可以输入相同的测试用例,同时运行方法a和方法b,假如二者得出的结果始终一致,就是对的。

2,比较器(重载运算符)

编程语言提供的排序函数sort,有正排序(从小到大)也有倒排序。其中第一个参数是需要排序的数组,第二个参数就是比较器。

即,人为的给出一个条件,用于判断哪个数据该放在前面。

比较器返回负数则第一个数放前面,返回正数第二个数放前面。

function student(name,age){
  this.name = name;
  this.age = age;
}

let arr = [new student('a',12),  new student('b',20)]
const sortFunc= Array.sort(arr, new condition());

function condition(a,b){ //比较策略
 return a.age-b.age
}

数据结构

 1,链表

 根据1可以得出,如果只涉及改、查数据,使用数组速度快; 如果涉及增、删使用链表更快。

链表和数组一样,都是地址引用。所以以下代码会导致原来链表改变:

// 已知一个链表node,头节点是head
const newNode = new ListNode(0,head);
newNode.next = newNode.next.next; //该行代码将node链表的第二个节点给删除了

2,堆

堆也是完全二叉树。堆分为大根堆(每个小树里面最大的值是根),小根堆(每个小树里面最小的值是根)。 

堆在硬件存储中是以数组的形式存储的,数组是一个连续的存储空间,连续的存储空间如何形成二叉树结构?实际上二叉树的父子节点之间使用数组下标来表示,有这样的对应关系:

数组下标:0,1,2,3,4,5,6;

树结构:

 

 那么父子节点之间有这样的对应关系,一个节点的下标是i,则其左子节点下标是2*i+1,其右子节点下标是2*i+2。同理i位置的父节点=(i-1)/2。比如1位置的左子节点=2*1+1=3

即,堆是以完全二叉树的形式在数组中存储的结构。

堆相关的三种题型:

向大根堆中push一个数字,重新排序成大根堆。(heapinsert)
修改大根堆中某个下标的数字,重新排序成大根堆。(heapify)
删除大根堆中某个下表的数字,重新排序成大根堆。(heapify)

由于二叉树的高度 与数字长度N的关系:H = logN,以上三种题型算法事件复杂度就是logN

堆排序就是heapinsert加heapify的过程。 

3,二叉树

种类:满二叉树,完全二叉树,二叉搜索树,平衡二叉搜索树(map,set数据结构的底层原理)
存储方式:链式存储,线性存储
构造一个二叉树:

function Node(val,left,right){
   this.val=val;
   this.left=left;
   this.right=right;
}

const a = new Node(1,null,null);
a.left = new Node(2);
a.right = new Node(3);

搜索方式:

深度优先搜索(递归):前,中,后序遍历 
广度优先搜索

算法思维基础
 

1,回溯算法

 回溯一般伴随着递归。

回溯能解决的问题:组合,切割,子集,排列,棋盘

所有的回溯都可以抽象为二叉树的结构:树的宽度是for循环的次数,树的深度是递归次数

模板:

function loop(){
 if(终止条件){
    收集结果;
    return 
}
for(集合数组){
  处理单层节点;
  递归操作;
  回溯操作;
}
return;
}

回溯三部曲:确定递归的参数和返回值、确定终止条件,单层搜索

注意点:

1,回溯法的递归值一般是没有返回值的(void)
2,递归必须有个终止条件,在终止条件中一般回收结果,然后return。(一般的在子集问题中在每个子节点中回收结果,除了子集问题,其余4个问题是在终止条件中回收结果。)
3, 接下来一般是for循环。(for循环用于遍历数组的每个元素)
4,其中的递归操作用于实现嵌套for循环。应该嵌套多少层for循环,要在if结束条件中去做决定
5,最后结束for循环执行return的操作。

经典例题:leetcode第77题组合,leetcode第46题全排列

 回溯算法解决组合问题的关键点是for循环的开始位置,一般取一个动态的startIndex,因为组合不包含顺序,顺序不同结果是一样的。[1,2]=[2,1]

回溯算法解决排列问题的关键点是for循环的开始位置始终是0,因为排列是包含顺序的,顺序不同算不同的结果([1,2]不等于[2,1])。另外排列需要一个辅助used数组用来记录原数组哪些数字是被使用过的,used=[1,0,1]就表示1和3被使用了,还剩下2可以用。

2,动态规划

解决的题型:斐波那契数列,爬楼梯,背包问题,打家劫舍,股票买卖,子序列问题,编辑距离问题

动态规划解题核心步骤
确定dp[i] 数组以及下标的含义(dp[i]表示第i个台阶有dp[i]种方法)
确定递推公式 (dp[i]=dp[i-1]+dp[i-2])
dp数组如何初始化(dp[1]=1,dp[2]=1)
遍历顺序(从前往后,还是从后往前遍历)
打印dp数组(为了检查代码到底哪里写错了)

3,递归:

解决题型:括号生成

递归三部曲:

  • 参数和终止条件
  • 返回值
  • 单层逻辑
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值