【墨尘の笔记】数据结构与算法

前言

仙人指路

[第一章]为入门,讲解数据结构是干什么的

[第二章]到第四章是数据结构基础,主要讲解链表,栈,队列,串等

[第五章]是二叉树

[第六章]是图,此章偏难,非考研或算法从业可粗略看

[第七章]到第八章是查找以及各种排序

[第九章]为补充,教材中没有的部分概念我随机补充上去

墨尘の话

本文内容主要参考王道考研,其中21版本咸鱼学长的课尚可,20版本比较枯燥,可选择性学习

浙大陈越姥姥的课比较精简且侧重于练习,能看懂且时间多的蛋疼可以看这个

因时间关系,后期比较匆促,本人也就是学着玩玩╮(╯▽╰)╭,可多参考链接自主学习

参考视频

2020 王道考研 数据结构 - B站

数据结构-浙江大学 - B站

第一章

1.1.1 数据机构的基本概念

1. 什么是数据结构

数据:信息的载体,是描述客观事务属性的数,字符,以及所有能输入到计算机种并被计算机程序识别和处理的符号集合

数据对象: 具有相通性质的数据元素的集合,是数据的一个子集

数据元素: 数据的基本单位,通常作为一个整体进行考虑和处理

数据项: 构成数据元素的不可分割的最小单位

举例:一群人,就是一个数据对象,每个人就是一个数据元素,他们的手,头等就是数据项

结构:数据中存在某种关联关系,称为结构

举例:小明是八年级一班中的学生,成绩为100

上例中小明和成绩是数据项,一班是数据元素,八年级是数据对象

2. 数据结构三要素

1. 逻辑结构

线性结构

线性结构: 数据元素是一对一的关系,除了第一个元素,所有都有唯一前驱(前面有一个节点);除了最后一个元素,所有元素都有唯一后继(后面有一个节点)

非线性结构

集合: 一组元素属于同一个集合,除此外别无关系

树形结构: 数据元素之间是一对多的关系

图状结构: 数据元素之间是多对多的关系

image-20210326163346644

2. 存储结构

顺序存储

顺序存储: 把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系靠存储单元的邻接关系体现

非顺序存储

链式存储: 逻辑上相邻的元素在物理位置上可以不相邻,借助元素存储地址的指针来表示元素之间的逻辑关系

索引存储: 在存储元素信息的同时,还建立附加的索引表。索引表中每项称为索引项,索引项一般为(关键字,地址)

散列存储: 根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储

顺序存储便于查找,非顺序查找便于增删

image-20210326163403699

3. 数据的运算

image-20210326163428469

3. 概念

image-20210326163444137

1.2.1 算法的基本概念

1. 什么是算法

程序 = 数据结构 + 算法

数据结构负责将现实世界的问题转化为信息,然后将信息存入计算机

算法是处理信息的步骤,负责将信息进行处理得到我们想要的结果

2. 算法的特性

  1. 有穷性: 有限时间里可以执行完
  2. 可行性: 可以用现有的操作实现算法

好算法的特性

  1. 正确性:正确解决问题
  2. 可读性:别人也能很清晰的看明白
  3. 健壮性(鲁棒性): 可以处理异常情况,比如非法数据等
  4. 高效率: 省时间,省内存

3. 概念

image-20210326164240364

1.2.2 时间复杂度

1. 如何评价算法的时间开销

1.和机器性能相关,诸如超级计算机 vs 单片机

2.和编程语言相关,越高级的语言效率越低

3.和编译程序产生的机器指令相关

4.有些算法是不能事后统计的

通常情况机器性能和编程语言无法改变,因此考虑如何优化程序性能即可

时间开销与问题规模n之间的关系

2. 如何计算

一般指代循环/迭代,循环的次数越多时间复杂度越高(参考下图)

image-20210326164348922

3. 三种复杂度

1.最坏时间复杂度: 考虑最坏的情况

2.平均时间复杂度: 考虑所有输入情况都处于同等概率

3.最好时间复杂度: 考虑最好的情况

评定一个算法的时间复杂度好坏通常要拿最坏情况去考虑

4. 概念

image-20210326164401093

1.2.3 空间复杂度

通俗理解:每个变量都会在内存中开辟一个新的空间,因此基于空间复杂度优化变量越少越好,递归尤甚

内存开销与问题规模n之间的关系

基于现在计算机内存来讲,通常我们只需要考虑时间复杂度,空间复杂度无需作为首选考虑

1. 概念

image-20210326164503662

第二章

2.1 线性表的定义的基本操作

1. 线性表的定义

线性表是具有相同数据类型的n(n >= 0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表

image-20210326164516562

2. 线性表的基本操作
// 初始化表。构造一个空的线性表L,分配内存空间
InitList(L); 
// 销毁操作。销毁线性表,并释放线性表L所占用的内存空间
DestroyList(L); 

// 插入操作。在表L中第i个位置上插入指定元素e
ListInsert(L, i ,e);
// 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值
ListDelete(L, i ,e);

// 按值查找操作。在表L中查找具有给定关键字值的元素。
LocateElem(L, e);
// 按位查找操作。获取表L中第i个位置的元素的值。
GetElem(L, i);

// 其他常用操作

// 求表长。返回线性表L的所有元素值。
Length(L);
// 输出操作。按前后顺序输出线性表L的所有元素值。
PrintList(L);
// 判空操作。若L为空表,则返回true,否则返回false
Empty(L);

Tips:

1.对数据的操作(分析思路) —— 创销,增删改查(适用于所有数据结构)

2.java函数的定义 —— <返回值类型> 函数名(<参数1类型> 参数1,<参数2类型> 参数2,…)

3.实际开发中,可根据实际需求定义其他的基本操作(输出,判空等)

4.函数名和参数的形式,命名都可改变(Reference: 严蔚敏版《数据结构与算法》)

概念

image-20210326164531526

2.2.1 顺序表的定义

1. 顺序表的定义

用顺序存储的方式实现线性表

顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现

2. 顺序表的特点

优点

  1. 随机访问,既可以在O(1)时间内找到第一个元素

  2. 存储密度高,每个节点只存储数据元素

    缺点

  3. 拓展容量不方便(采用动态分配时间复杂度较高)

  4. 插入,删除不方便,需要移动大量元素

3. 概念

注:图上所示函数均为c语言中动态分配相关函数,可根据自身情况选择性学习

代码在2.2.2.2 顺序表的查找一节

image-20210326164618599

2.2.2.1 顺序表的插入删除

概念

image-20210326164640102

2.2.2.2 顺序表的查找

/**
 * @Description: 顺序表
 * @author: HouBo
 * @Date: 2020/11/26 17:52
 */
public class SqList {
   

    static final int MAXSIZE = 10; // 最大长度
    int data[] = new int[MAXSIZE]; // 使用静态数组存放数据
    int length;  // 顺序表的当前长度

    // 初始化一个顺序表
    void initList(SqList L){
   
        L.length = 8; // 初始长度为0
    }

    // 添加一个数据
    boolean listInsert(SqList L, int i, int e){
   
        if(i < 1 || i > L.length + 1){
    // 当前i的值是否有效
            return false;
        }
        if(L.length == MAXSIZE){
    // 当前存储空间是否充盈
            return false;
        }
        for(int j = L.length; j >= i; j--){
    // 将第i个元素之后的元素后移
            L.data[j] = L.data[j - 1];
            L.data[j - 1] = e; // 在位置i处放入e
        }
        L.length++; // 长度+1
        return true;
    }

    // 删除一个数据
    boolean listDelete(SqList L, int i, int e){
   
        if(i < 1 || i > L.length + 1){
    // 当前i的值是否有效
            return false;
        }
        e = L.data[i - 1];
        for(int j = i; j <= L.length; j++){
    // 将第i个元素之后的元素后移
            L.data[j - 1] = L.data[j]; // 在位置i处放入e
        }
        L.length--; // 长度-1
        return true;
    }
    
    // 按位查找
    int getElem(SqList L, int i){
   
        if(i < 1 || i > L.length + 1){
   
            return 0;
        }
        return L.data[i - 1];
    }

    // 按值查找
    int locateElem(SqList L, int e){
   
        for(int i = 0; i < L.length; i++){
   
            if(L.data[i] == e){
   
                return i + 1;
            }
        }
        return 0;
    }
}

此段代码非最优解,仅作练习使用

概念

image-20210326164703240

2.3.1 单链表的定义

1. 什么是单链表

相对于顺序表,单链表除了存储数据元素外,还需要存储指向下一个节点的指针

image-20210326164749030

2. 单链表和顺序表的不同
顺序表 单链表
优点 随机存取,存储密度高 不要求大片连续空间,改变容量方便
缺点 要求大片连续空间,改变容量不方便 不可随机存取,要耗费一定空间存放指针
带头结点和不带头结点的区别

创立第一个结点,既为带头结点,反之则为不带

不带头结点,对第一个数据结点和后续数据结点的处理需要用不用的代码逻辑,空表和非空表的处理同理

带头结点通常情况下优于不带

概念

代码放在2.3.2.3 单链表的建立一节中

image-20210326164901618

2.3.2.1 单链表的插入删除

此章中,p通过代指当前结点,s指新插入的结点, a1指下一个结点, e指新值

1.1 按位序插入(带头结点)

思路:插入结点打断链表,上一个结点挂到它身上(指向它),它挂到下一个结点身上。

链表插入删除操作和顺序表相比相对复杂,需多多思考。

image-20210326165108584

1.2 按位序插入(不带头结点)

不带头结点第一个结点需要做特殊处理,创建一个s,将第a1挂到它身上。

2. 指定结点的后插操作

a1挂到s身上,s挂到p身上。

3. 指定结点的前插操作

由于无法直接获取p的上一个结点,因此可采用如下方法实现:

思路:将p变更为s, s变更为p

实现:p的值赋给s,新值e赋给p

4. 按位序删除

思路:找到要删除的结点,断开它并释放空间

5. 指定结点的删除

思路:同上

实现:将p的值和下一个引用都指向下一个,实现删除p的效果

此处如果p是最后一个结点,将会出现异常

单链表的局限性:无法逆向检索,不方便

6. 概念

image-20210326165124409

2.3.2.2 单链表的查找

概念

image-20210326165137520

2.3.2.3 单链表的建立

思路:遍历所有结点,通过头插或者尾插的方式实现循环建立一个单链表。

头插法可以实现反转链表,注意这是个重点

代码
/**
 * @Description: 结点
 * @Author: MoChen
 */
public class HNode {
   

    public HNode(){
   

    }

    public HNode(int data){
   
        this.data = data;
    }

    // 数据
    int data;
    // 下一个结点
    HNode next;
}

/**
 * @Description: 单链表
 * @Author: MoChen
 */
public class HLinkerList {
   

    // 链表的头结点
    HNode head = null;

    // 初始化一个空链表
    boolean InitList(HLinkerList L){
   
        L.head = new HNode(); // 分配一个头结点,如果不带头结点此处直接赋空
        L.head.next = null; // 头结点之后暂时没有结点
        return true;
    }

    // 在第i个位置插入元素e
    boolean ListInsert(HLinkerList L, int i, int e) {
   
        if(i < 1) {
   
            return false;
        }
        // 以下代码段为不带头结点的操作
//        if(i == 1) { // 插入第一个结点的操作与其他结点操作不同
//            HNode s = new HNode();
//            s.data = e;
//            s.next = L.head.next;
//            L.head = s; // 头指针指向新结点
//        }
//        HNode p; // 指针p向前扫描到的结点
//        int j = 0; // 当前p指向的是第几个结点,不带的话此处赋1
//        p = L.head; // L指向头结点,头结点是第0个结点
//        while (p != null && j < i - 1) { // 循环找到 i - 1 个结点
//            p = p.next;
//            j++;
//        }
        HNode p = GetElem(L, i - 1);

//        if(p == null){ // i值不合法
//            return false;
//        }
//        HNode s = new HNode();
//        s.data = e;
//        s.next = p.next;
//        p.next = s; // 将结点s连到p之后
//        return true; // 插入成功

        return InsertNextNode(p, e);
    }

    // 后插操作:在p结点之后插入元素e
    boolean InsertNextNode(HNode p, int e){
   
        if(p == null){
   
            return false;
        }
        HNode s = new HNode();
        s.data = e; // 新结点s存储数据e
        s.next = p.next;
        p.next = s; // 新结点s挂到p身上
        return true;
    }

    // 前插操作:在p结点之前插入元素e
    boolean InsertPriorNode(HNode p, int e){
   
        if(p == null){
   
            return false;
        }
        HNode s = new HNode();
        s.next = p.next;
        p.next = s; // 新结点s挂到p身上
        s.data = p.data; // 将p里面的元素复制到s中
        p.data = e; // p中的元素覆盖为e
        return true;
    }

    // 按位序删除
    boolean ListDelete(HLinkerList L, int i, int e){
   
        if(i < 1){
   
            return false;
        }
//        HNode p; // 指针p向前扫描到的结点
//        int j = 0; // 当前p指向的是第几个结点,不带的话此处赋1
//        p = L.head; // L指向头结点,头结点是第0个结点
//        while (p != null && j < i - 1) { // 循环找到 i - 1 个结点
//            p = p.next;
//            j++;
//        }
        HNode p = GetElem(L, i - 1);

        if(p == null){
    // i值不合法
            return false;
        }
        if(p.next == null){
    // 第i - 1个结点后面已无其他结点
            return false;
        }
//        HNode q = p.next; // 令q指向被删除的结点
//        e = q.data; // 用e返回元素的值
//        p.next = q.next; // 将q结点从链中断开
//        return true; // 删除成功

        return DeleteNode(p);
    }

    // 删除指定结点p
    boolean DeleteNode(HNode p){
   
        if(p == null){
   
            return false;
        }
        HNode q = p.next; // 令q指向p的后继结点
        p.data = p.next.data; // 和后继结点交换数据域
        p.next = q.next; // 将q结点从链中断开
        return true;
    }

    // 按位查找,返回第i个元素
    HNode GetElem(HLinkerList L, int i){
   
        if(i < 0){
   
            return null;
        }
        HNode p; // 指针p向前扫描到的结点
        int j = 0; // 当前p指向的是第几个结点,不带的话此处赋1
        p = L.head; // L指向头结点,头结点是第0个结点
        while (p != null && j < i) {
    // 循环找到 i 个结点
            p = p.next;
            j++;
        }
        return p;
    }

    // 按值查找
    HNode LocateElem(HLinkerList L, int e){
   
        HNode p = L.head.next;
        // 从第一个结点开始查找数据域为e的结点
        while(p != null && p.data != e){
   
            p = p.next;
        }
        return p; // 找到后返回该结点,否则返回null
    }

    // 求表的长度
    int Length(HLinkerList L){
   
        int len = 0;
        HNode p = L.head;
        while(p.next != null){
   
            p = p.next;
            System.out.println("链表第" + len + "个数据:" + p.data);
            len++;
        }
        return len;
    }
}

此段代码非最优解,仅作练习使用

2.3.3 双链表

下文中,s指代新结点,p指代当前结点,q指代下一个结点

1. 双链表和单链表的概念

单链表:无法逆向检索,有时候不太方便

双链表:可进可退,存储密度更低

双链表整体和单链表差距不大,额外新增了一个指向上一个结点的处理

2. 双链表的插入

思路:和单链表同理,额外增加了prior的处理

① q挂到s身上

② q的prior指向s

③ s的prior指向p

④ s挂到p身上

如果q不存在,第二步需省略

image-20210326165204033

3. 双链表的删除

① q的下一个结点挂到p身上

② q的下一个结点的prior指向p

image-20210326165214894

代码
/**
 * @Description: 结点(双链表)
 * @Author: MoChen
 */
public class HDNode {
   
    public HDNode(){
   

    }

    public HDNode(int data){
   
        this.data = data;
    }

    // 数据
    int data;
    // 下一个结点
    HDNode next;
    // 上一个结点
    HDNode prior;
}
/**
 * @Description: 双链表
 * @Author: MoChen
 */
public class HDLinkedList {
   

    // 链表的头结点
    HDNode head = null;

    // 初始化一个空链表
    boolean InitList(HDLinkedList L){
   
        L.head = new HDNode(); // 分配一个头结点,如果不带头结点此处直接赋空
        L.head.prior = null; // 头结点的prior永远指向空
        L.head.next = null; // 头结点之后暂时没有结点
        return true;
    }

    // 在p结点之后插入s结点
    boolean InsertNextHDNode(HDNode p, HDNode s){
   
        if(p == null || s == null){
    // 非法参数
            return false;
        }
        s.next = p.next;
        if(p.next != null){
    // 如果p后面有结点
            p.next.prior = s;
        }
        s.prior = p;
        p.next = s;
        return true;
    }

    // 删除p结点的后继结点
    boolean DeleteNextNode(HDNode p){
   
        if(p == null){
   
            return false;
        }
        HDNode q = p.next; // 找到p的后继结点q
        if(q == null){
   
            return false; // p没有后继
        }
        p.next = q.next;
        if(q.next != null){
    // q结点不是最后一个结点
            q.next.prior = p;
        }
        return true;
    }

    // 求表的长度
    int Length(HDLinkedList L){
   
        int len = 0;
        HDNode p = L.head;
        while(p.next != null){
   
            p = p.next;
            System.out.println("链表第" + len + "个数据:" + p.data);
            len++;
        }
        return len;
    }
}

概念

image-20210326165233947

2.3.4 循环链表

1. 循环链表与普通链表的不同

循环链表的尾结点的指针指向头结点,而普通链表的尾结点指向空。

通常我们无法获取之前的结点,除非拿到头结点,循环单链表可以通过遍历结点的方式从而拿到自己想要的结点。

以前在增加/删除操作时,需要给最后一个结点判空,现在则不需要。

2. 循环双链表

表头的prior指向表尾,表尾的next指向表头。

概念

image-20210326165256412

2.3.5 静态链表

1. 什么是静态链表

单链表:各个结点在内存中随意散落。

静态链表:分配一整片连续的内存空间,各个结点集中安置。

image-20210326165336687

2. 静态链表的概念

静态链表就是用数组的方式实现的链表。

容量固定不可变

本章内容不是很重要,了解其概念即可。

2.3.6 顺序表和链表的比较

1. 线性结构

都属于线性结构

2. 存储结构
顺序表 单链表
优点 随机存取,存储密度高 不要求大片连续空间,改变容量方便
缺点 要求大片连续空间,改变容量不方便 不可随机存取,要耗费一定空间存放指针
3. 基本操作
操作 顺序表 链表
需要分配大片连续空间。若分配空间过小,则之后不方便扩展;若分配空间过大,则浪费内存资源 只需分配一个头结点(也可不分配),方便扩展
长度赋0即可,本笔记采用java实现,因此无需考虑内存问题,如果使用c实现则需要通过free函数释放内存 依次删除每个结点
增,删 插入/删除元素都要将后续元素全部后移/前移 时间复杂度O(n),时间开销主要来自移动元素 插入/删除元素只需修改指针即可 时间复杂度O(n),时间开销主要来自查找元素
按位查找:O(1) 按值查找:O(n)
4. 概念

表长无法预估,需要经常进行增/删操作,可使用链表

表长可预估,需要经常进行查询操作,可使用顺序表

第三章

3.1.1 栈的基本概念

1. 栈(Stack)的概念

栈是只允许在一端进行插入或删除的线性表

重要术语:栈顶,栈底,空栈

image-20210326205646609

特点:后进先出(Last In First Out)LIFO

2. 栈的基本操作
InitStack(S); // 初始化栈。构造一个空栈,分配内存空间。
DestroyStack(L); // 销毁栈。销毁并释放栈s所占用的内存空间。

Push(S, x); // 进栈,若栈S未满,则将x加入使之成为新栈顶。
Pop(S, x); // 出栈,若栈S非空,则弹出栈顶元素,并用x返回。

GetTop(S, x); // 读栈顶元素。若栈S非空,则用x返回栈顶元素。

// 其他常用操作
StackEmpty(S); // 判断一个栈是否为空
概念

image-20210326205510127

3.1.2 栈的顺序存储实现

思路

给定一个静态数组,每次操作头或者尾即可

指针用于操纵当前数据

image-20210326210119682

代码
/**
 * @Description: 顺序存储实现栈
 * @Author: MoChen
 */
public class HSqStack {
   

    final int MAXSIZE = 10; // 定义栈中元素的最大个数

    int[] data; // 静态数组中放栈中元素

    int top; // 栈顶指针

    HSqStack(){
   
        data = new int[MAXSIZE];
    }

    // 初始化栈
    void InitStack(){
   
        top = -1; // 此处如果指向0,则预示已经有一个参数了,所以初始化指向-1
    }

    // 判断栈空
    boolean StackEmpty(){
   
        return top == -1;
    }

    // 入栈
    boolean Push(int x){
   
        if(top == MAXSIZE - 1){
    // 栈满
            return false;
        }
        top++; // 指针上移一位
        data[top] = x; // 新元素入栈
        return true;
    }

    // 出栈
    boolean Pop(int x){
   
        if(top ==  - 1){
    // 空栈
            return false;
        }
        x = data[top]; // 栈顶元素先出栈
        top--; // 指针下移
        return true;
    }

    // 读取栈顶元素
    int GetTop(int x){
   
        if(top ==  - 1){
    // 空栈
            return -1;
        }
        x = data[top]; // 读取栈顶元素
        return x;
    }
}
概念

image-20210326205743322

3.1.3 栈的链式存储实现

用链表的方式实现栈几乎等同于单链表,此处直接套用单链表即可

3.2.1 队列的基本概念

1. 什么是队列

栈(Stack)是只允许在一端进行插入或删除的线性表

队列(Queue)是只允许在一端插入,在另一端删除的线性表

image-20210326210328613

队列的特点:先进先出 First In First Out (FIFO)

2. 队列的基本操作
InitQueue(Q); // 初始化队列,构造一个空队列Q
DestroyQueue(Q); // 销毁队列,销毁并释放队列Q所占用的空间

EnQueue(Q, x); // 入队,若队列Q未满,将x加入,使之成为新的队尾
DeQueue(Q, x); // 出队,若队列Q非空,删除队头元素,并用x返回

GetHead(Q, x); // 读队头元素,对队列Q非空,则将队头元素赋值给x
QueueEmpty(Q); // 判断队列是否为空
概念

image-20210326210404480

3.2.2 队列的顺序实现

要点

求队列一共有多少个元素的算法如下:

// 尾指针 + 长度 - 头指针 % 长度
(rear + MAX_SIZE - front) % MAX_SIZE

头尾指针均为长度以内任意一个数字,因此想要求差必须先加上最大长度

尾指针和头指针位置可调换

概念上不会大于长度,保证代码健全性加上模运算

代码
/**
 * @Description: 顺序队列
 * @Author: MoChen
 */
public class HSqQueue{
   

    private static class SqQueue{
   
        private final int MAX_SIZE = 10;
        int[] data; // 静态数组存放元素
        int front, rear; // 队头指针和队尾指针

        public SqQueue(){
   
            data = new int[MAX_SIZE];
        }
    }

    private SqQueue Q;

    // 初始化队列
    void initQueue(){
   
        Q = new SqQueue();
        Q.front = Q.rear = 0; // 初始时,队头队尾全部指向空
    }

    // 判断队列是否为空
    boolean queueEmpty(){
   
        return Q.front == Q.rear; // 当队头队尾指向一样时,则意味队列为空
    }

    // 入队
    boolean enQueue(int x){
   
        if((Q.rear + 1) % Q.MAX_SIZE == Q.front){
    // 如果队头的指针的下一位指向队尾,则表示队满
            return false;
        }
        Q.data[Q.rear] = x;
        Q.rear = (Q.rear + 1) % Q.MAX_SIZE; // 队尾指针+1取模,保证指针不会超过长度
        return true;
    }

    // 出队
    boolean deQueue(){
   
        if(Q.front == Q.rear){
    // 队列为空
            return false;
        }
        Q.data[Q.front] = -1;
        Q.front = (Q.rear + 1) % Q.MAX_SIZE; // 队尾指针+1取模,保证指针不会超过长度
        return true;
    }

    // 获取队头元素
    int getHead(){
   
        if(Q.front == Q.rear){
    // 队列为空
            return -1;
        }
        int x = Q.data[Q.front];
        return x;
    }

    // 获取队列长度
    int queueLength(){
   
        return (Q.rear + Q.MAX_SIZE - Q.front) % Q.MAX_SIZE;
    }
}

代码不完善,仅作练习使用

概念

image-20210326210438044

3.2.3 队列的链式实现

概念

链式无需考虑内存

代码
/**
 * @Description: 链式队列
 * @Author: MoChen
 */
public class HLinkQueue {
   

    private class LinkNode{
   
        int data;
        LinkNode next;

        public LinkNode(){
   }

        public LinkNode(int data) {
   
            this.data = data;
        }
    }

    private class LinkQueue{
   
        LinkNode front, rear;
    }

    private LinkQueue Q;

    // 初始化队列
    void initQueue(){
   
        // 初始时,front,rear都指向头结点
        Q.front = Q.rear = new LinkNode();
        // 不带头结点的话front和rear都要指向空
        Q.front.next = null;
    }

    // 判断队列是否为空
    boolean isEmpty(){
   
        return Q.front == Q.rear;
    }

    // 入队
    boolean enQueue(int x){
   
        LinkNode s = new LinkNode(x);
        s.next = null;
        // 如果不带头结点则front需要进行非空判断,为空则修改表头指针
        Q.rear.next = s; // 新结点挂到rear身上
        Q.rear = s; // 修改表尾指针
        return true;
    }

    // 出队
    boolean deQueue(){
   
        if(Q.rear == Q.front){
    // 空队列
            return false;
        }
        LinkNode p = Q.front.next;
        Q.front.next = p.next; // 修改头结点的next指针
        if(Q.rear == p){
    // 如果是最后一个结点
            Q.rear = Q.front;
        }
        return true;
    }
}
概念

image-20210326210514330

3.2.4 双端队列

概念

栈:单端插入和删除的线性表

队列:一端插入,另一端删除的线性表

双端队列:两端插入,两端删除的线性表

双端队列包含输入受限和输出受限两种情况,不同情况具有不同局限性,需根据情况选择

概念

image-20210326212039635

3.3.1 栈在括号匹配中的应用

示例:

一组括号,求出它们能不能完成匹配

思路:

使用栈,将左括号压入,如果遇到右括号就压出,遇到无法匹配的括号或者匹配结束栈里还有值则匹配失败

image-20210326212212977

代码
/**
 * @Description: 给定一组括号,判断这组括号能否完成匹配
 * @Author: MoChen
 */
public class Brancket {
   

    static String bracketStr = "({}{})";

    public static void main(String[] args) {
   
        System.out.println(bracketCheck(bracketStr));
    }

    static boolean bracketCheck(String bracketStr){
   
        char[] data = new char[bracketStr.length()]; // 栈元素
        int top = 0; // 栈顶指针
        char[] strs = bracketStr.toCharArray();
        for(int i = 0; i < strs.length; i++){
   
            if(strs[i] == '[' || strs[i] == '{' || strs[i] == '('){
    // 扫描到左括号,入栈
                data[++top] = strs[i];
            }else{
   
                if(top == 0){
    // 扫描到右括号且当前栈空,匹配失败
                    return false;
                }
                char topElem = data[top--]; // 栈顶元素出栈
                // 进行匹配,如果匹配失败则返回false
                if(strs[i] == ')' && topElem != '('){
   
                    return false;
                }
                if(strs[i] == ']' && topElem != '['){
   
                    return false;
                }
                if(strs[i] == '}' && topElem != '{'){
   
                    return false;
                }
            }
        }
        return top == 0; // 括号校验完成且栈空则匹配成功
    }
}

3.3.2 栈在表达式求值中的应用(上)

三种表达式的不同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcNMwH0u-1618913822522)(https://i.loli.net/2021/02/01/2GOCL9hukZWgNse.png)]

中缀表达式

运算符在中间,既为我们常用的数学表达式

后缀表达式

运算符在后面,又叫逆波兰表达式(Reverse Polish notation)

前缀表达式

运算符在前面,因为是波兰人提出,所以又叫波兰表达式(Polish notation)

中缀表达式 后缀表达式 前缀表达式
a + b ab + + ab
a + b - c a b + c - - + ab c
a + b - c * d ab + cd * - - + ab * cd

核心思想还是一个计算区间一块区域,诸如(a + b) - (c * d) = (ab+)(cd*) -

中缀转后缀

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UCf07h9Q-1618913822523)(https://i.loli.net/2021/02/01/t2GmHCKyBF59gvq.png)]

左优先原则:只要左边的运算符能先计算,就先计算左边的

后缀表达式的手算方法

从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应操作,合并为一个操作数

对于计算机而言,后缀表达式的效率往往要更高

中缀表达式转前缀表达式

右优先原则:只要右边的操作符能先计算,就优先计算右边的

中缀
A + B * (C - D) - E / F
常规计算
- + A * B - C D / E F
右优先
+ A - * B - C D / E F
后缀表达式的机算方法

① 从左到右扫描下一个元素,直到处理完所有元素

② 若扫描到操作数则入栈

③ 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果入栈

概念

image-20210326212341082

3.3.2 栈在表达式求值中的应用(下)

中缀转后缀

① 遇到操作数,直接加入后缀表达式

② 遇到界限符。遇到 ( 直接入栈,遇到 ) 则依次弹出栈内运算符并加入后缀表达式,直到弹出 ( 为止。注意( 不加后缀

③ 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 ( 或栈空则停止。之后再将当前运算符入栈。

中缀的计算

中缀转后缀 + 后缀求值

① 初始化两个栈,操作数栈和运算符栈

② 若扫描到操作数,压入操作数栈

③ 若扫描到运算符或界限符,则按照‘中缀转后缀’相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要在弹出两个操作数栈的栈顶元素并执行相应运算,运算结果在压回操作数栈)

代码

因时间原因,此处仅列出中转后和后计算

import java.util.Stack;
import java.util.regex.Pattern;

/**
 * @Description: 后缀表达式
 * @Author: MoChen
 */
public class PolishNotation {
   

    // 中缀表达式
    private static String notation = "((15 / (7 - (1 + 1))) * 3) - (2 + (1 + 1))";
    // 后缀表达式
    private static String poNotation = "15 7 1 1 + - / 3 * 2 1 1 + + -";
    // 前缀表达式
    private static String rpoNotation = "- * / 15 - 7 + 1 1 3 + 2 + 1 1";
    // 10的倍数
    private static int[] exactNums = {
   10, 100, 1000, 10000, 100000};

    public static void main(String[] args) {
   
        System.out.println("中计算:" + countNum(notation));
        System.out.println("中转后:" + commTransRpo(notation));
        System.out.println("后计算:" + poCount(poNotation));
    }

    /**
     * 中缀转后缀
     */
    static String commTransRpo(String notation
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值