数据结构与算法-线性表

599 阅读10分钟

一.数据结构

  • 什么是数据结构? 数据结构是计算机存储,组织数据的方式

数据结构分为三种:线性结构,树形结构,图形结构,每种结构包括的类型如下:

数据结构.png

二.线性表

  • 线性表是具有n个相同类型元素的有限序列(n>=0)

线性表.png

  • a1是首节点(首元素),an是尾节点(尾元素)
  • a1是a2的前驱,a2是a1的后继
  • 常见的线性表有数组,链表,栈,队列,哈希表(散列表)

三.数组(Array)

  • 数组是一种顺序存储的线性表,所有元素的内存地址也是连续的

数组内存空间.png

  • 数组缺点:无法动态修改数组容量

动态类设计需求 设计原则:

动态数组设计原则.png 接口设计如下:

  • size 数组存放成员变量的个数

  • clear 数组成员变量为泛型时,如果是基本数据类型时,只设置size=0,如果为对象数据类型时,不仅要设置size=0,还要遍历循环设置数组每一个成员变量的值为null

  • add 默认将数据加到数组的最后一位。直接将element添加到index=size,然后size++

  • 打印 重写对应的print方法,自定义打印内容,比如视频中讲到在java中打印数组,默认打印的是类名,默认调用数组的toString方法,如果想改变打印内容,重写toString方法,用StringBuilder自定义内容。了解思路即可

  • remove 数组成员变量为泛型时,如果是基本数据类型,删除index=n的数据时,将index=n后面的数据依次往前移动,移动范围为index=n-size-1;如果是对象数据类型,删除index=n的数据时,将index=n后面的数据依次往前移动,移动范围为index=n-size-1,然后size--,最后删除数组最后一个数组,这里size--的作用是通过size表示最后一个数组的下标值

  • add_index 首先在旧数组index=n的位置插入一个数据,先在堆空间中重新开辟一个内容,新size=旧size+1,然后遍历旧数组,遍历范围为(旧size-1)到index=n,将旧数组中index =(旧size-1)的数据移动到新数组size的位置,最后在新数组index=n的位置插入新数据

  • indexOf 如果传入数据为null,只需要将原数组遍历循环找到下标为null的index即可;如果传入数据不为null,将传入数据跟原数组中的数据进行遍历循环,通过equals方法比较是否相等,返回对应的index即可

  • 接口测试 Assert测试接口,或者直接打印数据

  • 动态扩容 动态扩容的思路是重新申请一块连续地址的内存空间,扩容的空间大小是原数组的n倍,然后将原数组中的数据拷贝到新数组中,销毁原数组的内存空间.

  • 泛型 泛型的目的让动态数组更加通用,Array,此处的E就为泛型,java中的泛型只能放对象数据类型,基本数据类型不可用,例如Int不能用,要用Int的包装类Integer,int=integer。声明数组中要写明泛型类型,

  • 对象类型 数组中存放对象类型数据时,实际存储分布为: 数组中存放的是对象的内存地址,内存地址又指向堆空间中分配的对象

数组中存放对象类型数据.png

  • equals java(视频中以java语言为基础讲的内容)中通过equals方法,判断比较对象是否相等,如果没有重写equals方法,默认比较对象的内存地址,如果重写equals方法,则可以指定比较的类型
  • null值处理

是否可以存储null的数据?答案是可以的,传入一个null ,默认index=n的位置上的数据为null,但是取出index=n的数据可能会报错,因为对象为null

  • 动态数组有一个明显的缺点:可能会造成内存空间的大量浪费

四.链表(Linked List)

  • 链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的
  • 相比动态数组浪费空间,链表是用到时才会分配存储空间
  • 数据存储表示如下:

链表.png

  • 动态设计链表 原则:在编写链表过程中,要注意边界测试,index=0,size,size-1等情况.

  • size

  • clear 首先size=0,然后设置first指针=null,不需要设置Node的next指向null

  • add 在index=n的位置插入一个Node元素,让index=n-1的Node中的next指针指向新插入的Node元素,新插入的Node元素中的next指针指向原index=n的Node元素,最后进行size++

  • remove 如果index=0,找到first,first.next.next指向index=index+1的位置;如果index!+0 删除index=n的元素,首先先获取index-1的元素,然后将获取的元素next指针指向index=n+1的元素

  • 获取index对应的节点 获取index的节点,需要从first指针指向的第一个结点算起,index等于几就.next几次,遍历循环,i=0;i<index node = node.next ,返回最后的node即可。

  • indexOf 如果元素为null,遍历链表获取Node,然后对比Node里面存储的数据是否为null;如果不为null,遍历链表获取Node,然后对比Node里面存储的数据;

  • 练习-删除链表中的节点 删除本节点的数值,改变本节点next指针指向,将要删除的node.next的下一个节点的值赋值给node,将node.next.next指针指向赋值给node.next指针

  • 练习-反转一个单向链表 一般考虑反转单向链表的实现效果如下:

Snip20210805_19.png

递归方式: 创建的这个算法 递归思路.png

非递归方式: 首先声明一个newHead指针指向null,然后将head.next指针指向newHead指针指向的位置,然后将newHead指针指向head对应的节点,再将head指向head.next下一个节点,这里有一个问题,将head.next指向newHead的时候,下一个节点就没有指针牵引,所有我们在最初的时候创建一个tmp指针,指向head.next,所以具体代码看下图:

非递归思路.png

  • 练习-判断一个链表是否有环

利用快慢指针来判断,如果有环,fast跟slow 会相遇,类似操场跑圈,否则没环。 链表有环.png

视频中java代码如下:

代码.png 注意点:while中的两个条件缺一不可,是为了避免fast或者fast.next为null后出现空指针异常的错误。

  • 虚拟头节点 有时候为了让代码更加精简,统一所有节点的处理逻辑,可以在最前面增加一个虚拟的头节点(不存储数据) 结构图如下:

虚拟头节点.png

虚拟头节点表示方法.png

note:添加虚拟头节点之后,first指的是虚拟头节点,first.next表示index=0 的节点

  • 数组复杂度分析 复杂度分析一般分为三个方向:最好情况复杂度,最坏情况复杂度,平均情况复杂度。 数组跟链表的复杂度分析主要分析增(add)删(remove)改(set)查(get)等方法。

先看数组:

get方法代码:

数组get方法.png

get方法时间复杂度为O(1)。

set方法代码:

数组set方法.png

set方法时间复杂度为O(1)。

add方法代码:

数组add方法.png

add方法时间复杂度分为三种:

最好的情况:在最后一位add元素,不需要移动元素,所有时间复杂度为O(1);

最坏的情况:在第一位add元素,需要移动整个数组元素,所有时间复杂度为O(n),此处的n代表的是数据规模,不是数据值,数组size是多少就移动多少次;

平均的情况:在中间任何位置add元素,需要移动的次数为:(1+2+3...+n)/n=1/2*n ,所有时间复杂度为O(n);

remove方法代码:

数组remove方法.png

remove方法时间复杂度分为三种:

最好的情况:在最后一位remove元素,不需要移动元素,所有时间复杂度为O(1);

最坏的情况:在第一位remove元素,需要移动整个数组元素,所有时间复杂度为O(n),此处的n代表的是数据规模,不是数据值,数组size是多少就移动多少次;

平均的情况:在中间任何位置remove元素,需要移动的次数为:(1+2+3...+n)/n=1/2*n ,所有时间复杂度为O(n);

数组复杂度总结:如果是set/get元素,时间复杂度为O(1);如果是add/remove元素,时间复杂度平均情况为O(n)

  • 链表复杂度分析

  • 数组,链表复杂度比较结果如下

复杂度分析.png

  • 均摊复杂度 连续出现多次复杂度比较低的情况后,出现个别复杂度比较高的情况,使用均摊复杂度,离诶是最好情况下的均摊复杂度

均摊复杂度分析.png

  • 数组的缩容 如果内存比较紧张,动态数据有比较多的剩余空间,可以考虑进行所容操作;

  • 复杂度震荡

五.双向链表

可以提高链表的综合性

双向链表.png

  • clear
size=0;
first=null;
last=null;

  • add

函数add.png

  • remove

函数remove.png

  • 接口测试 Assert 测试
  • 双向链表 VS 单向链表 同样的删除操作,复杂度相同的情况下,双向链表比单向链表的操作步骤少一半,所有双向链表效率高

删除次数比较.png

  • 双向链表 VS 动态数组

链表跟数组比较.png

image.png

六.单向循环链表

单向循环链表.png

  • add add的时候要考虑size=0 的时候 函数add.png

  • remove 要区分size=1 的情况 函数remove.png

  • 接口测试 Assert 测试

七.双向循环链表

双向循环链表.png

  • add

函数add.png

  • remove

函数remove.png

  • 静态链表

静态链表.png

  • 数组的优化思路

五.栈(Stack)

  • 1,栈的概念
  • 栈是一种特殊的线性表,只能在一端进行
  • 往栈中添加元素的操作,一般叫做push,入栈
  • 从栈中移除元素的操作,一般叫做pop,出栈(只能移除栈顶元素,也叫做:弹出栈顶元素)
  • 此处的栈跟栈空间是不同的概念,现在说的栈是一种数据结构,栈空间是一种内存
  • 优先使用动态数组自定义栈

栈结构图.png

栈结构示意图.png

  • 2.栈数据结构接口设计 根据push跟pop的原则,不希望设计那么稀奇古怪的接口

自定义栈接口设计.png

  • 栈的应用-浏览器的前进和后退 利用两个栈存放数据

后退:将1中的栈顶元素弹出放进2中

前进:将2中的栈顶元素弹出放进1中

新输入数据:只要新输入元素,不管2中有多少数据,一律清空,将新输入元素放进1中,作为栈顶元素

栈应用例子.png

  • 练习-有效的括号 利用栈先进先出的特点来解答这个问题

有效括号思路.png

代码实现

栈实现有效括号.png

hashmap实现有效括号.png

六.队列(Queue)

  • 队列是一种特殊的线性表,只能在头尾两端进行操作
  • 队尾(rear):只能从队尾添加元素,一般叫做enQueue,入队
  • 队头(front):只能从队头移除元素,一般叫做deQueue,出队
  • 先进先出的原则,First In First Out, FIFO
  • 优先使用双向链表实现自定义队列,因为队列主要是往头尾操作元素,用双向链表实现的队列效率会高于用动态数组实现的队列

队列结构图.png

  • 队列接口设计

接口设计.png

  • 练习——用栈实现队列 利用两个stack来做

用栈实现队列.png

七.双端队列(Deque)

  • 双端队列是能在头尾两端添加,删除的队列

  • 英文deque是 double ended queue 的简称

  • 双端队列接口设计

接口设计.png

八.循环队列(Circle Queue)

  • 循环队列底层使用数组实现,也可以看作是数组的优化结果
  • 循环队列默认是单端的
  • 循环双端队列:可以进行两端插入,删除操作的循环队列

循环队列.png

  • 自定义循环队列代码

循环队列代码.png

  • 循环队列扩容 扩容原则是将原来队头(front)指向新内存的index=0 的位置,依次往队尾方向存储

循环队列扩容.png

九.循环双端队列

讲课见视频