前端算法进阶 --006

174 阅读12分钟

一、 大话数据结构

1.1 数据结构绪论

数据组成

数据元素:组成数据的基本单位。
数据项:数据元素可以由若干个数据项构成,是数据不可分割的最小单位。
数据对象:数据相同的元素组成的集合,是数据的子集合。
example:
[类数据:人类, 元素:{张三, 李四,...}, 数据项:{眼睛,鼻子,...}, 数据对象(眼睛):{张三的眼睛, 李四的眼睛...}]

数据结构
不同的元素的数据元素之间不是相互独立的,而是存在关系的,这种关系称之为结构。

逻辑结构:集合、线性、树形、拓扑图
物理结构:数据的逻辑结构在计算机中的存储形式

顺序存储结构:数据存储在对地址连续的存储单元,逻辑关系与物理关系一致。
连式存储结构:数据元素位置任意,需要指针存放数据元素的地址信息。

1.2 算法

1.21 时间复杂度

算法的时间复杂度:T(n),随着n的变化,执行次数的变化,仅仅关注T(n)的最高阶。O(1):常数阶、O(n):线性阶、O(n2n^2)平方阶 。下面分别给出不同等差数列求和为例:

# 1+2...+n
# O(1)
def sum_1(n):
    return int((1 + n) * n / 2)

# n
def sum_2(n):
    count = 0
    for i in range(1, n+1):
        count += i
    return count

# n*n
def sum_3(n):
    count = 0
    for i in range(1, n+1):
        for j in range(i):
            count += 1
    return count

常用的算法复杂度比较:
O(1)<O(n)<O(logn)<O(nlogn)<O(n2)<O(n3)<O(2n)O(1) < O(n) < O(logn) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
最坏复杂度和平均复杂度:n个数顺序存储,现在需要插入某个元素,最好的情况1, 最坏的情况n+1,那么平均复杂度是多少了?,搜寻任意x的期望值(n+2)/2。一般没有特殊说明,复杂度都是指最坏的情况。

1.22 空间复杂度

定义和时间复杂度类似,重要的是我们可以通过空间复杂度来换取时间。
example:设计一个算法,复杂度为O(n2n^2), 这里把算法的结果顺序存储下来,复杂度O(1)。

1.3 线性表

最大的缺点是插入和删除:算法时间复杂度大。为了解决这个问题就得牺牲下空间---连式存储

  • 连式存储
    每个数据储存叫节点Node,Node由数域和指针域,前者存储数值后者存储后继的地址信息。存在头指针,每次必须从头指针开始,最后一个指针域为空。
  • 搜寻
    想得到某个指定地址数据,每次必须从头开始,往下搜寻下去找到即停止,否者直到指针域为空停止说明数据不存在。时间复杂度O(n).
  • 插入
    p节点后插入s节点,s->next=p->next, p-next=s,p的后继赋值给s后继,p后继改为s
  • 删除
    p->next = p->next->next
    ps:静态链表,循环链表,双向链表

1.4 栈

想象下弹夹

  1. 是线性单链表
  2. 仅仅允许在链表尾部,即栈顶进行插入和删除操作
  3. 1-n数依次进栈(仅仅是进去的顺序),出站顺序必须满足任意想编号x后面的编号小于x的必须倒序排列。
  4. 两个站共享内存,从中间某个位置作为两个栈顶, 两端为栈为。如果容积为n当两个站的顶部地址差为1时代表栈满。
  5. 链栈不存在栈满的情况???
  6. 栈的递归运用,斐波那契数列数列为例,每次递归到最底层就把数据压入栈的底部,慢慢向上回溯到顶部得到最后顶部值。
# 方法1
def Fib():
    a = 1
    yield a
    b = a
    yield b
    while 1:
        c = a + b
        yield c
        a = b
        b = c

# 这里采用递归的方法解决,稍微改变下需求,求第i个fib数列
def Fib_i(i):
    if i <= 2:
        return 1
    else:
        return Fib_i(i-1) + Fib_i(i-2)

1.5 串(string)

  • 朴素的模式匹配方法
    001 匹配000000001,从第一个起点index=0开始001,发现前两个00能匹配到第1个不行,然后index=1,直到尾部,如果两个长度分别为m, n (n >= m),则算法复杂度为(n - m + 1)* m。
  • KMP模式匹配
    abcd和abcabcd进行匹配

step1:
abcabcd
abcd
index=3时不匹配
step2
abcabcd
`abcd
index=1终止
step3
abcabcd
``abcd
index=2终止
!!!!!!其实上面step2 step3都是无效
因为在step1时a=a,向后一个个平移到abcabcd第二个a时才可以进一步可能,因此step2可以直接跳到下面
abcabcd
···abcd

1.6 二叉树

  • 二叉树的性质
  1. 第i层节点数最多可能:2i12^{i-1}
  2. 深度为n的二叉树至多:2n12^{n}-1个节点
  3. 叶节点为nleafn_{leaf},度为2 的节点数nnode2n_{node2},则nleaf=nnode2+1n_{leaf}=n_{node2}+1
  4. n个节点完全二叉树的深度[log2(n)]+1[log_2(n)] + 1,[]为取整运算
  5. 把完全二叉树n个节点按层进行编号。如果i=1则为root,i>1这这个节点的父节点为[i/2][i/2];如果节点2i>n2*i > n就没有左孩子,否则左孩子为2i2*i;如果2i+1>n2 * i +1 > n则无孩子,否则其有孩子为2i+12 * i +1.
  • 满树

n个节点,除了每个叶节点,每个节点都有左右左右子树,并且所有的叶节点都在最底层,层次m与节点n存在 2m1=n2^m - 1=n关系。

  • 完全二叉树

对应一个二叉树按层次对所以节点编号包括叶节点,如果所有节点编号与同样深度的满二叉树编号结果一致,那么该树为满二叉树。
性质、属性:

  1. 叶节点只能在最下面2层,倒数第二层存在叶节点则,一定集中在树右侧且连续,二最下面一层则集中在左侧且连续。
  2. 同样节点数的树,完全二叉树深度最小

满树是完全二叉树的子集

  • 二叉树的遍历
  1. 前序:根节点左孩子右孩子
  2. 中序:左孩子根节点右孩子
  3. 后续:左孩子右孩子根节点
  4. 层次遍历:顾名思义吧

1.7 排序

内排序: 与下相反
外排序: 需要辅助空间

排序方法:
1.冒泡排序:两两比较,不符合则交换,复杂度O(n2)O(n^2)
下面例子对比两者的时间:# out:0.34375643730163574; 0.10623884201049805

def buddleSort(L):
    n = len(L)
    for i in range(n):
        count = 0
        for j in range(n - 1):
            if L[j] > L[j + 1]:
                c = L[j]
                L[j] = L[j + 1]
                L[j + 1] = c
                count = 1
            else:
                pass
        if count == 0:
            # 如果序列不在发生改变跳出循环
            break
    return L


print(buddleSort([4, 3, 1, 2]))

# 升级算法:比如:
# 3, 2, 1, 4, 5   初始状态
# 2, 1, 3, 4, 5   第一个外层循环后,内层在j=2时后面的顺序不在改变
# 1, 2, 3, 4, 5   在第二外层,j=2和后面都不要在遍历了,前面一个外层已经知道
def buddleSort2(L):
    n = len(L)
    m = n
    for i in range(n):
        count = 1
        # print('m', m, i)
        flag = None
        for j in range(m - 1):
            if L[j] > L[j + 1]:
                c = L[j]
                L[j] = L[j + 1]
                L[j + 1] = c
                count = 0
                flag = j + 1
            else:
                pass
        # print('flag:', flag)
        m = flag
        if count:
            # 如果序列不在发生改变跳出循环
            break
    return L


Z = [1, 4, 3, 2] + list(range(10, 1000000))
print(Z[:7])
s = time.time()
res = buddleSort(Z)
cost = time.time() - s
print(Z[:7])    # 行数调用的时候吧Z顺序已经改变了,fuck

Z = [1, 4, 3, 2] + list(range(10, 1000000))
s = time.time()
res2 = buddleSort2(Z)
cost2 = time.time() - s
print(cost, cost2)
print(res2[:7])
  1. 简单排序法:找到最小的排在第一位,在剩下的元素中在找最小的排在第二位。。。。。复杂度O(n2)O(n^2)
  2. 直接插入排序: (利用链式存储)复杂度O(n2)O(n^2)参考
    (bubkoo.com/2014/01/15/…)

**

def insert(i, j, L):
    # i < j
    c = L[j]
    for z in range(j, i - 1, -1):
        L[z] = L[z - 1]
    L[i] = c
    return L
x = [1, 2, 4, 5]
print(insert(0, 3, x))

def InsertSort(L):
    n = len(L)
    if L[0] > L[1]:
        L = insert(0, 1, L)
    for i in range(2, n):
        for z in range(i - 1, -1, -1):
            if (L[i] < L[z]) and (L[i] >= L[z-1]):
                L = insert(z, i, L)
    return L

x = [1, 4, 1, 3, 4, 6, 2]
print(InsertSort(x))
  1. 希尔排序: 将数据分为d组,然后组内进行插入排序,d递减步长为1直到循环到1即停止,复杂度O(n3/2)O(n^{3/2})。[参考]
  2. 堆排序: 将数据编号按层次构建完全二叉树,利用满二叉树性质,从叶节点从树的下面向上寻,吧大的数给父节点,最后root就为最大的数,如此反复,就会得到序列。复杂度O(nlog2n)O(nlog_2n)
  3. 归并排序:分而治之复杂度O(nlog2n)O(nlog_2n)
  4. 快速排序: 参考**复杂度O(nlog2n)O(nlog_2n)

二、数据结构与算法

2.1 基本概念

2.11 数据结构定义

数据:电脑上存储的东西都是数据。图片是图像数据,歌曲是声音数据。编程语言中用int、double、char定义的变量都是数据。

数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。比如在人类中,数据元素是指一个人 。

结构:简单的理解就是关系。比如分子结构,就是说组成分子的原子之间的排列方式。严格点说,结构是指各个组成部分间相互搭配和排列的方式 。

数据结构是指相互之间存在着特定关系的的数据元素的集合

2.12 逻辑结构与物理结构

​ 类似于逻辑地址与物理地址。

(1)逻辑结构:是指数据元素之间的相互关系,与他们在计算机中存储的位置无关。 逻辑结构分为以下四种:

  • ①集合结构,元素间无关系。类似于数学中集合的概念。
  • ②线性结构,元素间一对一。
  • ③树形结构,元素间一对多。
  • ④图形结构,元素间多对多。

(2)物理结构:指数据元素在计算机磁盘中存储的方式。通常有顺序存储、链式存储、索引存储和哈希存储。

2.2 基本数据结构介绍

2.21 数组

数组(array),线性数据结构,在内存中顺序存储。 特点:高效的访问,低效的插入和删除。

2.22 栈(类似于子弹弹夹)

栈(stack),线性数据结构,元素只能先进后出(First In Last Out,简称FILO)。最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶(top)。

关于栈的操作有入栈(push)、出栈(pop)和遍历栈。

img

2.23 队列(类似于排队)

队列(queue),线性结构。队列的特性是:先进先出,后进后出。基本操作就是入队和出队。

**补充(循环队列):**由于队列既可以顺序存储又可以链式存储,当顺序存储时我们可以使用循环对列,来提高队列空间的使用效率。下面用动态图来演示下基本过程。

循环对列需要一个头指针front和一个尾指针tail来判断队列的状态。头指针始终指向队首元素,尾指针始终指向队尾元素的后一个位置, 并且尾指针指向的位置不存储元素,所以队列满时,其中的元素个数为capacity - 1。 当front == tail 时,说明队列为空;当front ==(tail + 1)% capacity时,队列满。(**注:**capacity指顺序存储时队列的总容量)

2.24 链表

链表在计算机中的存储方式为随机存储,不像数组是顺序存储。

链表也包括好几种,单链表、单向循环链表、双链表和双向循环链表。这里只介绍两种常用的链表。

(1)单链表 ​ 链表是由一个个结点所构成,先介绍单链表的一个结点。

单链表基本结构

image.png

基本操作有:插入操作、删除操作、更新操作和查找操作。

(2)双向链表 ​ 双向链表的结点比单链表的结点多一个前驱指针,一个结点有两个指针域。

image.png

2.25 树

树和图都是非线性数据结构,是由n(n>=0)个节点构成的有限集。n=0时称为空树。n>0时,有限集的元素构成一个具有层次感的数据结构。

基本概念:

  • 根节点:树的第一层的节点为根节点,根节点最多只有一个。
  • 叶子结点:无子节点的节点。
  • 父节点:有子节点的节点,如图中节点2为节点4和节点5的父节点。
  • 子节点:图中节点7、8为节点4的子节点,4、5又为节点2的子节点。
  • 树的高度(深度):从根开始定义起,根为第一层 , 根的孩子为第二层。根节点到离它最远的叶子节点为树的高度。
  • 节点的度:一个节点拥有 的子节点的个数,如节点4的度为2
  • 兄弟节点:如同一个父节点的节点彼此间为兄弟节点,如2与3、4与5、7与8

image.png

(1)二叉树 二叉树或者为空集,或者由一个根节点和两棵互不相交的、分别称为左子树和右子树的二叉树组成。从定义可以看出一棵二叉树:

二叉树是有序树,区分左子树与右子树,不可以随意交换子树位置。 一个节点的子树数量取值范围为0,1,2。0代表该节点是叶子节点,1代表该节点只有左子树或只有右子树,2代表该节点有左右子树。

满二叉树: 满二叉树的两个条件: ①一个二叉树的所有非叶子节点都存在左右孩子 ②所有的叶子节点都在同一层。 简单点说,满二叉树的每一个分支都是满的。

二叉树的性质:

  • 性质一:在二叉树的第i层上至多有2^(i-1)个节点(i>=1)
  • 性质二:深度为k的二叉树至多有2^k-1个节点
  • 性质三:对任何一棵二叉树T,如果终端节点数为n0,度为2的节点数为n2 ,那么 n0 = n2 +1
  • 性质四: 具有n个节点的完全二叉树的高度为至少为log2(n+1)
  • 性质五:如果对一棵有n个节点的完全二叉树的节点按层序编号(从第一层开始到最下一层,每一层从左到右编号),对任一节点i有:
      1. 如果i=1 ,则节点为根节点,没有双亲。
      1. 如果2 * i > n ,则节点i没有左孩子 ;否则其左孩子节点为2i . (n为节点总数)
      1. 如果2 * i+1>n ,则节点i没有右孩子;否则其右孩子节点为21+1

参考文献: 大话数据结构(程杰) 数据结构与算法之图解基本数据结构(一)