数据结构笔记

224 阅读12分钟

数据复杂度解决办法

  1. 暴力解决
  2. 修改冗余,复杂的代码
  3. 将时间复杂度变为空间复杂度

数据处理的基本操作

  1. 分析代码对数据先后进行那些操作
  2. 根据分析出来的操作,找到合理的数据解构

数据结构的概念

以什么方式对数据进行组织

1.线性表

  • 单行链表,双向链表,循环链表

  • 链表的结束结点是空指针,

  • 链表一方面存储值,一方面指向下一结点. 双向链表既要保存上一结点,又要保存下一结点,循环链表是结束结点指向开始结点,形成循环

  • 链表的新增和删除的时间复杂度都是0(1),即改变所要删除结点的指向,改变插入的结点的指向,

  • 链表的查询只能通过遍历链表,所以复杂度是0(n),

  • 链表的关键操作是快慢指针的操作, 慢指针一次跑一个结点,快指针一次两个结点,所以快指针到最后的时候,慢指针才一半.

  • 翻转链表的话,可以通过遍历链表,通过增加每个结点的prev(改变指向),形成一条指向上一结点的链表,然后再进行翻转

2.栈

一种特殊的线性表,对普通线性表的做了限制

  • 删除和新增都只能从后面进行操作,(后进先出)

  • 栈的表头叫栈底 表尾叫栈顶

  • 入栈操作叫 push 出栈叫pop

  • 栈的顺序结构一般可以通过数组来实现

  1. 数组的第一个元素放在栈底 数组的最后一个元素放在栈顶
  2. 定义一个top指针,表示当前栈顶,假如数组只有一个元素,top=0,一般以top是否为-1来判断是否为空栈
  3. 当定义了栈的最大容量(stacksize)时,则栈顶top必须小于stacksize

删除操作是top -1 ,增加操作是top+1,查找操作同于链表

链栈 以链表的形式来表示栈,

链栈和顺序栈的区别

  1. 相同点:操作极为相似,时间复杂度一样,都以为当前位置的指针进行数据对下的操作
  2. 新增和删除的对象,只能是栈顶的数据结点

栈的总结:

​ 高频使用删除,新增的操作时,而且新增或者删除的数据具有后来居上的关系的时,栈就是不错的选择

例如:浏览器的前进后退,括号匹配问题

3.队列

特殊的线性结构

​ 新增只能在末尾新增,删除只能删除第一个(先进先出),意思就是 删除前面的,因为前面的是之前就已经进来了,相当于后面挤出前面的

  1. 顺序队列,以数组形式形成的队列,在内存中是连续存储
  2. 链式队列,以指针的形式形成的链表,通过指向连成队列,尾进头出的单链表

数组队列

头指针和尾指针在一起时表示空队列,新增结点要把尾指针往后移,头指针往后移,但是会出现数组溢出的操作,

  1. 可以通过移动数据改变头指针的操作来实现,牺牲时间复杂度 O(n)
  2. 开辟足够大的内存空间,保证不会产生数组越界,
  3. 形成循环链表,头指针往后移的情况,尾指针出现越界,就指向头指针之前,形成闭环,(最优解)

链表

  • 链式队列是一个单链表,同时增加了front和rear指针,链式队列通常会增加一个头结点,并让头指针指向头结点,而且头结点不存储数据,只是用来做标识

  • 链式队列新增的话,讲新增结点加入rear指针之后,并改变rear指针指向,

  • 删除的话,删除的是头结点之后的节点,front指针指向头结点之后的节点的后继结点,如果删除之后,除头结点之外,无任何结点,那么此时要让rear归位(指向头结点),不然rear指针会变成野指针.

  • 头结点的存在是为了让头指针和尾指针在队列为空时,不会变成野指针

队列总结

时间复杂度上

  • 循环队列和链式队列的新增.删除操作都为O(1)
  • 查找操作中,队列只能通过全局遍历的方法进行,需要O(n)的时间复杂度

空间性能方面

  • 循环队列必须有一个固定长度,因此存在存储元素数量和空间的浪费问题
  • 链式队列更为灵活一点

总结

  • 在可以确定队列长度最大值时,建议使用循环队列,
  • 无法确定队列长度是,应考虑使用链式队列

4.数组

存放若干个相同类型数据元素的容器

  • 数组可以把具有相同类型的元素,以一种不规则的顺序进行排列,这些排列好的同类数据元素的合集叫数组
  • 数组在内存中是连续存放的,数组内的数据,可以通过索引直接取到
  • 数组的索引就是对于数组空间
  • 在进行新增.删除.查询操作的时候,可以根据代表数组空间的索引进行,只要记录该数组头部的第一个数据位置,然后累加空间

数组在存储数据时是按顺序存储的,并且存储的内存也是连续的,这就是造成了它既有增删困难,查找容易的特点

栈和队列是加了限制的线性表,只能在特定的位置进行增删操作,相比之下,数组可以在任意位置增删数据

数组的新增操作

  1. 在数组的最后增加一个新的元素,此时新增一条数据后,对原数据产生没有任何影响,可以直接通过新增操作,赋值或插入一条新的数据即可.时间复杂度为O(1)
  2. 如果在数据中间的某个位置新增数据,那么情况不一样,因为新增了数据之后,会对插入元素位置之后的元素产生影响,具体为这些数据的位置需要依次往后挪动1个位置

删除类似与新增,删除末尾的不会对原数组产生太大影响,但是删除中间元素的话,会导致整个数组元素产生位置移动,

数组的查找操作

  • 由于索引的存在,数据基于位置的查找操作比较容易实现,可以通过索引值,直接在O(1)时间复杂度内查找到某个元素的位置
  • 基于数值的查找方法,数组没有优势,例如,查找数值为9的元素是否出现过,以及如果出现,索引值是多少,这样基于数值的查找,需要整体遍历一遍,需要O(n)的时间复杂度,

归纳数组增删查的时间复杂度

  • 增加: 若插入数据在最后,则时间复杂度为O(1),如果在中间某处插入数据,则时间负责度为O(n)
  • 删除:对应位置的删除,扫描全数组,数据复杂度为O(n)
  • 查找:如果只需根据索引值进行一次查找,时间复杂度为O(1),但是要在数组中查找一个数值满足指定条件的数据,时间复杂度为O(n)

链表和数组的区别

  1. 链表的长度是可变的,数组的长度是固定的,在申请的数组的长度时就已经在内存中开辟了若干个空间,如果没有引用ArrayList时,数组申请的空间永远是估计了数据的大小后才执行
  2. 链表不会根据有序位置存储,进行插入数据元素时,可以用指针来充分利用内存空间,数组是有序存储的,如果想要充分利用内存的空间就只能选择顺序存储,而且需要在不取数据.不删数据的情况下才能实现

5.字符串

由多个字符串组成的一个有序整体(n>=0)

s = "BEIJING"

​ 双引号作用是为了将串和其他结构区分开,字符串的逻辑结构和线性表不同之处在于字符串针对的是字符集,也就是字符串中的元素都是字符

  • 空串 指含有零个字符的串, 例如 s = ""
  • 空格串,只包含空格的串,空格串中包含的是空格,且空格中可以包含多个空格,例如 s = " "
  • 子串,串中任意连续字符组成的字符串叫该串的子串
  • 原串通常也称为主串,例如 a ="BEI" B = "BEIJING" ,a是B的子串

只有两个串的串只完全相同,这两个串才相等,即使两个字符串包含的字符完全一致,它们也不一定是相等的,例如 A= 'BEIJING' B='JINGBEI'

字符串的存储分为链式字符串和顺序字符串

  • 通过链表存储字符串数据,每几个字符串形成一个结点,最后以\0 表示结束
  • 一般使用数组的形式,连续存储字符串的内容

字符串的新增和删除

  • 在中间插入 时间复杂度是O(n),在最后插入是O(1),也就是连接字符串
  • 删除跟新增一样,在末尾删除时间复杂度是O(1),在中间删除是O(n)

字符串查找

​ 需要在字符串中进行遍历查找,时间复杂度是o(n)

子串在父串的查找

  • 需要判断子串整个在父串是否出现,不仅仅是单个字符是否出现,而是长度要与子串一致,并且字串符内容要一致
  • 遍历父串和子串,逐一对比,如果该字符一样,就继续比下去,如果不一样,中断子串循环,开始比对父串的下一个字符,重复之前比对子串的操作,直到父串比对完毕,时间复杂度是O(nm)
  • 如果是查找两个串是否有最长子串,既要在上一步的操作上增加一步,记录全局最长子串,及其长度的数量,最终的时间复杂度是O(nm2)m的平方
function findStrMostShow(): void {
  const a = "goodgoogle";
  const b = "google";
  let max_len = 0,
    maxSubstr = "";
  for (let i = 0; i < a.length; i++) {
    for (let j = 0; j < b.length; j++) {
      if (a[i] === b[j]) {          //找到两个串相同的字符
        for (let m = i, n = j; m < a.length && n < b.length; m++, n++) {      //双边同时进行后移比对,如果相同则一直比对下去,就是公共字符串
          if (a[m] !== b[n]) {
            break;
          }
          if (max_len < m - i) {      //新的位置 减去之前保存的位置 得到新串长度
            max_len = m - i;
            maxSubstr = a.substring(i, m+1);
          }
        }
      }
    }
  }
  console.log(maxSubstr);
}
findStrMostShow();

字符串总结

  • 字符串的逻辑结构和线性表极为相似,区别仅在于串的数据对象约束为字符集
  • 字符串的基本操作和线性表有很大差别:
    • 在线性表中,大多以'单个元素'作为操作对象
    • 在字符串中,通常以'串的整体'作为操作对象
    • 字符串的删除操作和数组很像,复杂度也与之一样,但字符串的查找操作就复杂很多

6.树

是由结点和边组成的,不存在环的一种数据结构

  • a是b和c的子父结点,那么b和c就是a的子结点
  • b和c 互为兄弟结点
  • 如果a没有父结点,那么a就是根结点
  • 如果a或者c没有子结点,那么称为叶子结点
  • 层数计算: 根结点为1层,根的子节点为2层,以此类推,树的最大层数为树的层深

二叉树

在二叉树中,每个结点最多有两个分支,即每个结点最多两个子结点,分别称作左子结点右子结点

  • 满二叉树: 除了叶子结点外,所有结点都有两个子结点
  • 完全二叉树:除了最后一层的结点外,其他的结点数量都达到最大,并且最后一层的叶子结点都靠左排列

二叉树存储方法

  1. 基于指针的链式存储法,像链表一样,每个结点有三个字段,分别存储数据,左结点,右结点的指针
  2. 基于数组的顺序存储法,按照顺序存储,根节点默认存储在下标为1的位置,然后是根结点的左子结点为2,根结点的右子结点为3,以此类推,会发现,如果结点X的下标为i,那么X的左子结点总是存放在2i的位置,X的右子结点总是存放在2i+1的位置
  • 数据结构都是'一对一'的关系,即前面的数据和下面的一个数据产生了一个连接关系,例如链表.栈,队列等
  • 树结构是'一对多'的关系,即前面的父结点,跟下面若干个子结点产生了连接关系

遍历树的三种方法:前序遍历,中序遍历,后序遍历

这里的序是指父结点的遍历顺序

  • 前序是先遍历父结点
  • 中序是中间遍历父结点
  • 后序是最后遍历父结点
  • 不管哪种遍历都是通过递归调用完成的

树的基本操作

  • 二叉树遍历中,每个结点都被访问了一次,其时间复杂度为O(n),
  • 找到位置后,执行增加和删除数据的操作时,只需要通过指针建立连接关系就可以
  • 对于没有任何特殊关系的二叉树而言,真正执行增加和删除操作的数据复杂度是O(1)树数据的查找和链表一样,都需要遍历每一个数据去判断,时间复杂度是O(n)

二叉查找树(也称作二叉搜索树)具备以下几个的特性:

  • 在二叉查找树种的任意一个结点,其左子树中的每个结点的值,都要小于这个结点的值
  • 在二叉查找树种的任意一个结点,其右子树中的每个结点的值,都要大于这个结点的值
  • 在二叉查找树中,会尽可能规避两个结点值相等的情况
  • 对二叉查找树实行中序遍历,就会输出一个从小到大的有序队列

利用二叉查找树执行查找操作时,可以进行一下判断:

  • 首先判断跟结点是否等于要查找的数据,如果是就返回
  • 如果根节点大于要查找的数据,就往左子树递归执行查找操作,直到叶子结点
  • 如果根节点小于要查找的数据,就往右子树递归执行查找操作,直到叶子结点
  • 这样的'二分查找'所消耗的时间复杂度可以降低为O(logn)

二叉查找树的插入操作

  • 从根节点开始,如果插入的数据比根节点大,且根节点的右子结点不为空,则在根节点的右子树中继续尝试执行插入操作,直到找到为空的子结点执行插入动作
  • 二叉查找树插入数据的时间复杂度是O(logn),这里的时间复杂度更多的是消耗在了遍历数据去找到查找位置上,真正执行插入动作的时间复杂度是O(1)

二叉查找树的删除操作

  • 情况一:如果删除的结点是某个叶子结点,则时间删除,将其父结点指针指向null
  • 情况二:如果要删除的结点只有一个子结点,只需将其父节点指向的子结点的指针换成其子结点的指针
  • 情况三:如果要删除要删除的结点有两个子结点,则有两种可行的操作方式
    1. 找到这个结点的左子树中最大的结点,替换成要删除的结点
    2. 找到这个结点的右子树中最小的结点,替换成要删除的结点

普通二叉树的查找操作时间复杂度都是O(n),二叉查找树的查找操作是O(logn)

7.哈希表

哈希表是一种特殊的数据结构,它与数组.链表以及树等数据相比,有明显的区别

哈希表的核心思想

数据存储的位置和数据的具体数据之间不存在任何关系,在面对查找文件时,这些数据结构必须采用逐一比较的方法去实现.哈希表的设计采用了函数映射的思想,将记录的存储位置与记录的关键字关联起来,不需要与表中存在的关键字进行比较后再进行查找

"地址 = f(关键字)"的映射关系,就是哈希表的核心思想

从本质上讲,哈希冲突只能尽可能减少,不能完全避免

哈希表需要设计合理的哈希函数,并对冲突有一套处理机制.

如何设计哈希函数

  1. 直接定制法:哈希函数为关键字找到地址的线性关系,如H(key)= a * key+b.这里a和b是设置好的常数
  2. 数字分析法:假设关键字集合中每个关键字key都是由s位数字组成(k1,k2 ,ks),并从中提起分步均匀的若干位组成哈希地址
  3. 平方取中法:如果关键字的每一位都有某些数字重复出现,并且频率很高,可以先求关键字的平方值,通过平方扩大差异,然后取中间几位作为最终存储地址
  4. 折叠法:如果关键字的位数很多,可以将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)作为哈希地址
  5. 除留余数法:预设一个关键字p,然后对关键字进行取余运算,即地址为key mod p

任何解决哈希冲突:

  • 开放定址法:当一个关键字和另一个关键字发生冲突时,使用某种探测技术在哈希表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中.常用的方法树线性探测法
  • 链地址法:将哈希值相同的记录存储在一张线性表中

哈希表的优势:

​ 它可以提供非常快速的插入-删除-查找操作,无论多少数据,插入和删除值都需要接近常量的时间,在查找方面,哈希表的速度要比树还要快,基本可以瞬间查找到想要的元素

哈希表的劣势:

​ 哈希表的数据是没有顺序概念的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素,哈希表的key是不允许重复的

8.递归

递归是指函数执行中使用函数自身的方法

  1. 递归问题必须可以分解成若干个规模比较小,与原问题形式相同的子问题,并且这些子问题可以用完全相同的解题思路解决
  2. 递归问题是一个对原问题一个从大到小拆解的过程,并且有一个明确的终点(临界点),最后从这个临界点开始,把小问题的答案按照原路返回,原问题便得到解决
  • 递归的基本思想是把规模大的问题转化为规模小的相同的子问题来解决
  • 在函数实现时,大问题的解决方法和小问题的解决方法是同一个方法
  • 解决问题的函数必须有明确的结束条件,否则就会有无线递归的情况
  • 递归的实现包括了两个部分:
    • 递归主体
    • 终止条件

递归的数学模型就是数学归纳法

递归的算法思想

  1. 当面对一个大规模问题时,如何把它分解为几个小规模的同样的问题
  2. 把当问题通过多轮分解后,最终的结果,也就是终止条件如何定义

当一个问题同时具备以下2个条件时,就可以使用递归的方法求解

  1. 可以拆解为除了数据模型以为,求解思路完全相同的子问题
  2. 存在终止条件

写出递归代码的关键在于,写出递推公式和找出终止条件

  1. 找到大问题分解成小问题的规律,并基于此写出递推公式
  2. 找出终止条件,就是当找到最简单的问题时,如何写出答案
  3. 将递推公式和终止条件翻译成实际代码
//汉诺塔
function main() {
  let x = "x",
    y = "y",
    z = "z";
  hanio(3, x, y, z);
}
function hanio(n: number, x: string, y: string, z: string) {
  if (n === 1) {
    console.log(`移动${x} => ${z}`);
  } else {
    hanio(n - 1, x, z, y);			//借助z把x的n-1移到y上
    console.log(`移动${x}=>${z}`);
    hanio(n - 1, y, x, z);		//借助x 把y的n-1 移到z上
  }
}
main();

9.分治

核心思想是"分而治之",把一个大规模,高难度的问题,分解为若干个小规模,难度低的小问题

可以采用同一种解法,递归的去解决这些子问题,再将每个子问题的解合并,就得到原问题的解

当需要采用分治法时,一般原问题需要具备以下几个特征:

  1. 难度在降低,即原问题的解决难度,随着数据的规模的缩小而降低
  2. 问题可分,原问题可以分解为若干个规模较小的同类型问题
  3. 解可合并,利用所有子问题的解,可合并出原问题的解
  4. 相互独立,各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题

二分查找实现,必须满足条件,所查找数据源必须是有序列表

二分查找的思路如下:

  1. 选择一个标志将集合L分为二个子集合,一般可以使用中位数
  2. 判断标志L(i)是否能与要查找的值des相等,相等则直接返回结果
  3. 如果不相等,需要判断L(i)与des的大小
  4. 基于判断的结果决定下步是向左查找还是向右查找,如果向某个方向查找的空间为0,则返回结果未查到

二分查找的经验和规律的总结

  1. 二分查找的时间复杂度是O(logn),这也是分治法普遍具有的特性
  2. 二分查找的循环次数并不确定,一般是达到某个条件就跳出循环编码的时候,多数采用while循环加break跳出的代码结构
  3. 二分查找处理的原问题必须是有序的

分治法总结

分治法经常会用在海量数据处理中,在面对默认问题时,需要注意:

  • 原问题的数据是否有序
  • 预期的时间复杂度是否带有logn项
  • 是否可以通过小问题的答案合并出原问题的答案

10.排序算法

排序 -- 让一组无序数据变成有序数据的过程,一般默认这里的有序都是从小到大的排列顺序

衡量一个排序算法的优劣:

  1. 时间复杂度 -- 具体包括最好时间复杂度,最坏时间复杂度以及平均时间复杂度
  2. 空间复杂度--如果空间复杂度为1,叫原地排序
  3. 稳定性--指相等的数据对象,在排序后,顺序是否能保证不变

冒泡排序

从第一个开始,依次比较相邻元素的大小,如果前者大于后者,则进行交换操作,把大的元素往后交换,通过多轮迭代, 直到没有交换操作为止,每次都吧最大的数据传递到最后

冒泡排序的性能

  • 冒泡排序最好时间复杂度是O(n),也就是当输入数组刚好是顺序的时候,只需要挨个比较一遍就可以
  • 冒泡排序最坏的时间复杂度是O(n*n),也就是当数组刚好是完全逆序的时候,每轮(排序都)需要挨个比较n次,并重复n次
  • 当输入数据杂乱无章时,它的平均时间复杂度是O(n*n)
  • 空间复杂度是O(1)

插入排序

选择未排序的元素,插入到已排序的区间的合适位置,直到未排序区间为空,

  • 插入排序的最好时间复杂度是O(n),即当数组刚好是完全顺序时,每次只用比较一次就能找到正确的位置区间
  • 插入排序最坏时间复杂度则需要O(nn),即当数组刚好是完全逆序时,每次都需要比较n次才能找到正确的位置区间,
  • 插入排序的平均时间复杂度是O(nn).因为往数组中插入一个元素的平均时间复杂度是O(n),而插入排序可以理解为重复n次的插入操作
  • 插入排序的空间复杂度是O(1)
function insertSort() {
  let arr = [2, 3, 5, 6, 1, 23, 6, 78, 34];
  for (let i = 1; i < arr.length; i++) {
    let temp = arr[i];
    let j = i - 1;
    for (; j >= 0; j--) {
      if (arr[j] > temp) {
        arr[j + 1] = arr[j];
      } else {
        break;
      }
    }
    arr[j + 1] = temp;
  }
  console.log(arr);
}
insertSort();

插入排序和冒泡排序的异同点

相同点:插入排序和冒泡排序的平均时间复杂度都是O(n*n),且都是稳定的排序算法,都属于原地排序

差异点:

  • 冒泡排序每轮的交换操作是动态的,所以需要三个赋值操作才能完成
  • 插入排序每轮的交换动作会固定待插入的数据,因此只需要一步赋值操作

归并排序

采用二分策略对数据进行二分处理,不断的进行二分操作,直到不能进行,然后将两块数据合并,

function main() {
  let arr = [59, 38, 64, 32, 12, 33, 55, 31, 11, 59];
  console.log("原始数据", arr);
  coustomMergeSort(arr, 0, arr.length - 1);
  console.log("归并排序", arr);
}

function coustomMergeSort(arr: any, start: number, end: number) {
  if (start < end) {
    let mid = parseInt(Number((start+end)/2).toFixed(2));
    coustomMergeSort(arr, start, mid);
    coustomMergeSort(arr, mid + 1, end);
    coustomDoubleSort(arr, start, mid, end);
  }
}
function coustomDoubleSort(a: any, left: any, mid: any, right: any) {
  let temp: Array<any> = [];
  let p1 = left,
    p2 = mid + 1,
    k = left;
  while (p1 <= mid && p2 <= right) {
    if (a[p1] <= a[p2]) {
      temp[k++] = a[p1++];
    } else {
      temp[k++] = a[p2++];
    }
  }
  while (p1 <= mid) {
    temp[k++] = a[p1++];
  }
  while (p2 <= right) {
    temp[k++] = a[p2++];
  }
  for (let i = left; i <= right; i++) {
    a[i] = temp[i];
  }
}
main();

归并排序的性能

  • 归并排序采用了二分的迭代方法,复杂度是logn
  • 每次的迭代,需要对两个有序数组进行合并,这样的动作在O(n)的时间复杂度下可以完成
  • 归并排序的复杂度是二者的乘积O(n*logn)
  • 归并排序最后.最坏.平均时间复杂度都是O(nlogn)
  • 归并排序的空间复杂度为O(n)
  • 是稳定的排序法

快速排序

快速排序的原理也是分治法,每轮迭代会选取数组中的某个元素作为分区点,将小于它的数据放在左边,大于他的数据放在右边,然后利用分治思想分别对他的左右两次进行操作,直至每个区间缩小为1

快速排序的性能

  • 在快排的最好时间的复杂度下,如果每次选取分区时,都能选中中位数,吧数组等分成两个,此时的时间复杂度是O(n*logn)
  • 在最坏的时间复杂度下,也就是如果每次都分区选中了最大值或者最小值,得到不等的两组,就需要n次的分区操作,每次分区平均扫描n/2个元素,此时时间复杂度退化为O(n*n)
  • 快速排序法平均复杂度是O(n*logn)
  • 快递排序法的空间复杂度为O(1)

排序算法的性能分析

  • 排序最暴力的方法,时间复杂度是O(n)
  • 利用归并排序能让时间复杂度降到O(logn)
  • 归并排序需要额外开辟临时空间:
    • 为了保证稳定性
    • 在归并时,由于在数组中插入元素导致了数组挪移的问题
  • 为了规避因此产生的时间损耗,此时采用快速排序,通过交换操作,可以解决插入元素导致的数据挪移问题,到时由于其动态二分的交换数据,导致了由此得出的排序结果并不稳定

总结

  • 对数据规模比较小的数据进行排序时,可以选择时间复杂度为O(n*n)的排序算法
  • 对数据规模比较大的数据进行排序时,可以选择时间复杂度为O(n*logn)的排序算法
    • 归并排序的空间复杂的为O(n),也对空间资源消耗会很多
    • 快速排序在平均时间复杂度为O(nlogn),最坏的时间复杂度也有可能逼近O(n*n).

动态规划算法

  1. 动态规划算法的核心思想是,将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法

  2. 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解

  3. 与分治不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是独立的,(即下一个子阶段的求解是建立在上一个子阶段的解基础上,进行进一步的求解)

  4. 动态规划可以通过填表的方式来逐步推进,得到最优解

    01234
    笔记本(1500g) 101500150015001500
    吉他(3000g) 401500150015003000
    电脑(2000g) 301500150020001500+200

背包问题的主要算法思想:利用动态规划来解决,每次遍历到第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中.即对于给定的n个物品,设v[i],w[i]分别为第i个物品的价值和重量,C为背包的容量,再令 v[i] [j]表示在表示在前i个物品中能够装入容量为j的背包中最大价值,

(1) v[i][0] =v[0][j] = 0;//表示 填入表 第一行和第一列是0
(2) 当w[i]>j时:v[i][j] = v[i-1][j] //当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上个单元格的装入策略
(3) 当j>=w[i]时:v[i][j] = max{ v[i-1][j] ,v[i]+v[i-1][j-w[i]] }
//当准备加入的新增的商品的容量小于等于当前背包的容量,
//填入方式
v[i-1][j]:就是上一个单元格的装入的最大值
v[i]:表示当前商品的价值
v[i-1][j-w[i]] :装入i-1商品,到剩余空间j-w[i]最大值
当j>=w[i]时: v[i][j] = max{ v[i-1][j],v[i]+v[i-1][j-w[i]] }
//动态规划背包问题
function getMax() {
  let w: Array<number> = [1, 4, 3];
  let val: Array<number> = [1500, 3000, 2000];
  let m = 4;
  let n = val.length;
  let v: Array<any> = new Array(n + 1);
  let path: Array<any> = new Array(n + 1);
  for (let i = 0; i < v.length; i++) {
    v[i] = new Array(m + 1);
    path[i] = new Array(m + 1);
  }
  for (let i = 0; i < v.length; i++) {
    for (let j = 0; j < v[0].length; j++) {
      v[i][j] = 0;
    }
  }
  //
  for (let i = 1; i < v.length; i++) {
    for (let j = 1; j < v[0].length; j++) {
      // 上一次取物件比我当前的重   重的比较贵
      if (w[i - 1] > j) {
        v[i][j] = v[i - 1][j];
      } else {
        //没有我当前中 就要判断  当前价格  和对比组合价格
        //当前设备加上之前的设备的价格 大于上一个物品价格
        // v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
        if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
          v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
          path[i][j] = 1;
        } else {
          v[i][j] = v[i - 1][j];
        }
      }
    }
  }
  console.log(v);
  console.log("==============");
  let i = path.length - 1;
  let j = path[0].length - 1;
  while (j > 0 && i > 0) {
    if (path[i][j] == 1) {
      console.log(`第${i}放入背包`);
      j -= w[i - 1];
    }
    i--;
  }
}

getMax();

kmp字符串匹配算法

KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法

KMP算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前端匹配过的位置,省去了大量的计算时间

function kmpMain() {
  let s1 = "BBC ABCDAB ABCDABCDABDE";
  let s2 = "ABCDABD";
  let next: Array<number> = kmpNext(s2);
  console.log(next);
  let index = kmpSearch(s1,s2,next)
  console.log(index);
}

function kmpSearch(s1: string, s2: string, next: Array<number>): number {
  let flag = -1;
  for (let i = 0, j = 0; i < s1.length; i++) {
    //   需要处理s1[i] !== s2[j]  去调整j的大小
    while (j > 0 && s1[i] !== s2[j]) {
      j = next[j - 1];
    }
    if (s1[i] === s2[j]) {
      j++;
    }
    if (j === s2.length) {
      flag = i - j + 1;
    }
  }
  return flag;
}


//获取部分匹配表
function kmpNext(dest: string): Array<number> {
  let next: Array<any> = new Array(dest.length);
  next[0] = 0;      //当字符串长度为1时 部分匹配值就是0
  for (let i = 1, j = 0; i < dest.length; i++) {
    // kmp算法的核心
    // 当dest[i] !== dest[j]时,我们需要从next[j-1]获取新的j  
    // 在部分匹配表中逐个去查找 是否当前值已经有部分匹配值
    while (j > 0 && dest[i] !== dest[j]) {
      j = next[j - 1];
    }
    //当满足dest[i] === dest[j]时,部分匹配值就为1
    if (dest[i] === dest[j]) {
      j++;
    }
    next[i] = j;
  }
  return next;
}

kmpMain();

贪心算法

  1. 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
  2. 贪婪算法所得到的结果不一定是最优的结果(有的时候会是最优解),但是都是对近似(接近)最优解的结果