数据结构与算法分析2.表、栈、队列、字符串

点击进入我的博客

1 绪论

线性结构的特点
  1. 在数据元素的非空有限集中,存在唯一的一个被称为第一个的数据元素
  2. 存在唯一的一个被称为最后一个的数据元素
  3. 除第一个之外,集合中的每个数据元素均只有一个前驱
  4. 除最后一个之外,集合中的每个数据元素均只有一个后驱
常用线性结构
  • 线性表
  • 队列
  • 双队列
  • 数组

2 线性表

线性表是n个数据元素的有限队列,同一线性表中的元素必定具有相同的特性,即属于同一数据对象,相邻数据元素之间存在着序偶关系。

  • 表中元素个数称为线性表的长度
  • 线性表没有元素时,称为空表
  • 表起始位置称表头,表结束位置称表尾

2.1 线性表的顺序表示和实现

线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,通常是用数组实现。在Java语言中,主要是java.util.ArrayList实现。

ArrayList初探
  1. ArrayList实现了List接口,继承了AbstractList,一般我们把它认为是可以自增扩容的数组。
  2. ArrayList是非线程安全的。
  3. 它实现了Serializable接口,因此它支持序列化。
  4. 实现了RandomAccess接口,支持快速随机访问(只是个标注接口,并没有实际的方法),这里主要表现为可以通过下标直接访问(底层是数组实现的,所以直接用数组下标来索引)。
  5. 实现了Cloneable接口,能被克隆。
  6. 底层实现:ArrayList底层通过一个Object[]来实现的。
  7. 默认初始容量:10
  8. 理论最大容量:Integer.MAX_VALUE - 8。之所以是理论最大容量是因为需要考虑JVM的内存,如果超出内存会报OutOfMemoryError的内存溢出错误;之所以要减去8是因为数组对象有一个额外的元数据,用于表示数组的大小。
  9. 默认扩容倍数:1.5倍。int newCapacity = oldCapacity + (oldCapacity >> 1);
  10. sizesize是数组中元素的数量
  11. modCountmodCountArrayList的父类AbstractList中的变量,默认值为0,记录的是关于元素的数目被修改的次数。主要作用是快速失败机制:由于ArrayList设计成非线程安全的,一个线程在对一个集合对象进行迭代操作的同时,其他线程可以修改此ArrayList,如可能引起迭代错误的add()remove()等危险操作。所以在AbstractList中,使用了一个简单的机制来规避这些风险,这就是modCountexpectedModCount的作用所在。
  12. Arrays.copyof():在扩容等操作时,需要通过此方法复制原数组内容到一个新容量的大数组里(Arrays.copyof方法实际是调用System.arraycopy方法)。

2.2 线性表的链式表示和实现

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的),所以对数据元素而言,除了存储其本身的信息之外,还需要一个指示其后继数据元素的信息。

循环链表
  • 循环链表:是另一种形式的链式存储结构,它的特点是表中最后一个结点的指针域指向头节点
  • 附加表头:因为不带附加表头在插入删除时要分两种情况,操作节点在表头和不在表头;而带了附加表头便可以对所有节点一视同仁。
  • 快慢指针(判断单链表是否为循环链表、判断链表中是否存在环):使用快慢2个指针,2个指针都从链表头开始遍历,快指针每次移动2个结点,慢指针每次移动1个结点。若链表中存在环,则快慢2指针后在链表的某一位置相遇,否则它们不会相遇。
    循环链表.png
双向链表
  • 双向链表的结点有两个指针域,其一指向直接后继元素,另一指向前驱元素。
  • 双向链表的目的是为了解决在链表中不同方向(前/后)访问元素的问题。
LinkedList初探
  1. LinkedList是一个继承于AbstractSequentialList双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
  2. LinkedList实现List接口,能对它进行队列操作。
  3. LinkedList实现Deque接口,即能将LinkedList当作双端队列使用。
  4. LinkedList实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  5. LinkedList实现java.io.Serializable接口,这意味着LinkedList支持序列化。
  6. LinkedList是非线程安全的。
  7. LinkedListNode信息:
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
  1. 由于LinkedList由于同时实现了ListDeque接口,所以有多种添加方法的底层是一样的。
  2. Linked允许元素为null
  3. Node<E> node(int index):该方法返回双向链表中指定位置处的节点,而链表中是没有下标索引的,要指定位置出的元素,就要遍历该链表,从源码的实现中,我们看到这里有一个加速动作。源码中先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历。

2.3 LinkedList与ArrayList的区别:

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  2. 对于随机访问get()和set(),ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
  4. ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间,就存储密度来说,ArrayList是优于LinkedList的。
  5. 当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

2.4 其他链表

广义表(Generalized List)
  • 广义表是线性表的推广
  • 对于线性表而言,n个元素都是基本的单元素
  • 广义表中,这些元素不仅可以是单元素也可以是另一个广义表
多重链表
  • 链表中的节点可能同时隶属于多个链
  • 多重链表中结点的指针域会有多个
  • 但包含两个指针域的链表并不一定是多重链表,比如双向链表不是多重链表
  • 多重链表有广泛的用途: 基本上如树、图这样相对复杂的数据结构都可以采用多重链表方式实现存储

3 栈

栈(Stack)是限定只能在表尾进行插入或删除的线性表。对栈来说,表尾称为栈顶,表头称为栈底。栈又称为后进先出线性表(LIFO,Last In First Out)。Java中由于java.util.Stackjava.util.Vector先天的设计问题,并不推荐使用;一般使用LinkedList来当作栈。
[图片上传失败...(image-b267ad-1582731953399)]
[图片上传失败...(image-72fd67-1582731953399)]

3.1 括号匹配

假设一个算术表达式中可以包含两种括号:圆括号和方括号,且这两种括号可按任意的次序嵌套使用,编写判别给定表达式中所含括号是否正确配对出现的算法。

步骤如下:
  1. 在算法中设置一个栈,每次读入一个括号;
  2. 若是右括号,则或者使置于栈顶的最急迫的期待得以消解,此时将栈顶的左括号弹出;或者是不合法的情况,此时将右括号压入(此处可以优化);
  3. 若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消解的期待的急迫性都降低一级;
  4. 在算法的开始和结束时,栈应该为空。
public static void main(String[] args) {
    String str = "[([[]])]";
    LinkedList<Character> stack = new LinkedList<>();

    for(char ch : str.toCharArray()) {
                if (stack.isEmpty()) {
                        stack.push(ch);
                } else if((ch == ']' && stack.peek() == '[') || ch == ')' && stack.peek() == '(') {
            stack.pop();
        } else {
            stack.push(ch);
        }
    }
    System.out.println(stack.isEmpty());
}

3.2 迷宫问题

迷宫
回溯算法

迷宫问题是栈的典型应用,栈通常也与回溯算法连用,回溯算法的基本描述是:

  1. 选择一个起始点;
  2. 如果已达目的地, 则跳转到 (4); 如果没有到达目的地, 则跳转到 (3) ;
  3. 求出当前的可选项;
    3.1 若有多个可选项,则通过某种策略选择一个选项,行进到下一个位置,然后跳转到 (2);
    3.2 若行进到某一个位置发现没有选项时,就回退到上一个位置,然后回退到 (2) ;
  4. 退出算法。
迷宫问题伪代码:

尚需说明一点的是,所谓当前位置可通,指的是未曾走到过的通道块,即要求该方块位置不仅是通道块,而且既不在当前路径上(否则所求路径就不是简单路径),也不是曾经纳入过路径的通道块(否则只能在死胡同内转圈)。

do {
    若当前位置可通,
    则 {
        将当前位置压入栈顶; // 纳入路径
        若当前位置是出口位置,则结束;//求得的路径已在栈中
        否则切换当前位置的东临块为新的当前位置;
    } 否则 {
        若栈不空,且栈顶位置尚有其它方向未搜索,
            则设定新的当前位置为沿顺时针方向旋转找到的栈顶位置的下一临块;
        若栈不空但栈顶位置四周均不可通,
        则 {
            删去栈顶位置;
            若栈不空,则重新测试新的栈顶位置,
               直到找到一个可通的相邻块或出栈至栈空;
        }
    }
}while(栈不空);

3.3 表达式求值

为实现算符优先算法,可以使用两个工作栈。一个称做OPTR,用以寄存运算符;另一个称做OPND,用以寄存操作数或运算结果。算法的基本思想如下:
(1) 首先置操作数栈OPND为空栈,表达式起始符"#"为运算符栈OPTR的栈底元素;
(2) 依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR的栈顶元素符比较优先权后作相应操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为"#")。

3.4 递归&汉诺塔

递归函数

一个直接调用自己或通过一系列的调用语句间接地调用自己的函数。

  1. 很多数学函数是递归定义的;
  2. 有的数据结构,如二叉树、广义表等,由于结构本身固有的递归特性,则它们的操作可递归地描述;
  3. 虽然问题本身没有明显的递归结构,但用递归求解比迭代求解更简单,如八皇后问题、Hanoi塔问题等。
N阶Hanoi塔

假设有3个分别命名为X、Y和Z的塔座,在塔座X上插有n阶Hanoi塔个直径大小各不相同、依小到大编号1,2,...,n的圆盘。现要求将X轴上的n阶Hanoi塔个圆盘移至塔座Z上并仍按同样顺序叠排,圆盘移动时必须遵循下列规则:

  1. 每次只能移动一个圆盘;
  2. 圆盘可以查在X、Y和Z中的任一塔座上;
  3. 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。

3.5 中缀表达式与后缀表达式

中缀表达式
  • 运算符号位于两个运算数之间
后缀表达式
  • 运算符号位于两个运算数之后
中缀转后缀:
  • 运算数相对顺序不变
  • 运算符号顺序发生改变
中缀转后缀方法:
  • 运算数——直接输出;
  • 左括号——压入堆栈:
  • 右括号——将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出)
  • 运算符——若优先级大于栈顶运算符时,则把它压栈;若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出,再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈
  • 若各对象处理完毕,则把堆栈中存留的运算符一并输出。

4 队列

  • 与栈相反,队列是一种先进先出(FIFO)的线性表。
  • 它只允许在表的一端进行插入,而在另一端删除元素。
  • 允许插入的一端叫队尾,允许删除的一端叫队头。
链队列——队列的链式表示

用链表表示的队列简称为链队列。一个链队列显然需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一确定。和线性表的单链表一样,为了操作方便起见,我们也给链队列添加一个头结点,并令头指针指向头结点。由此,空的链队列的判断条件为头指针和尾指针均指向头结点,如图所示:

链队列

循环队列——数组式队列改进

在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进:无论插入或删除,一旦rear指针增1或front指针增1时超出了所分配的队列空间,就让它指向这片连续空间的起始位置。自己真从MaxSize-1增1变到0,可用取余运算rear%MaxSize和front%MaxSize来实现。这实际上是把队列空间想象成一个环形空间,环形空间中的存储单元循环使用,用这种方法管理的队列也就称为循环队列。
在循环队列中,当队列为空时,有front=rear,而当所有队列空间全占满时,也有front=rear。为了区别这两种情况,规定循环队列最多只能有MaxSize-1个队列元素,当循环队列中只剩下一个空存储单元时,队列就已经满了。因此,队列判空的条件时front=rear,而队列判满的条件时front=(rear+1)%MaxSize。队空和队满的情况如图:


循环队列
双端队列

双端队列,是限定插入和删除操作在表的两端进行的线性表,尽管双端队列看起来比栈和队列灵活,但实际上在应用程序中远不及栈和队列有用。

5 字符串

  • 字符串:是由零个或多个字符组成的有限序列。
  • 字串:串中任意个连续的字符组成的子序列成为该串的字串。
  • 回文串:是一个正读和反读都一样的字符串,如abcbaabba

一年又一年,字节跳动 Lark(飞书) 研发团队又双叒叕开始招新生啦!
【内推码】:GTPUVBA
【内推链接】:https://job.toutiao.com/s/JRupWVj
【招生对象】:20年9月后~21年8月前 毕业的同学
【报名时间】:6.16-7.16(提前批简历投递只有一个月抓住机会哦!)
【画重点】:提前批和正式秋招不矛盾!面试成功,提前锁定Offer;若有失利,额外获得一次面试机会,正式秋招开启后还可再次投递。

推荐阅读更多精彩内容