408-数据结构-线性表、栈、队列(一)

本文是 “408数据结构” 的复习笔记中“线性表、栈、队列”的部分,主要依据王道的课本,考408的小伙伴可以拿走原笔记

有错误的地方还请各位留言指出,谢谢啦(ง •_•)ง

按照王道数据结构的章节目录,本篇文章有以下【三】章

  • 基础知识
  • 线性表
  • 栈与队列

基础

学习工具

可以使用这个可交互的数据结构学习网页

时间复杂度

算法中基本运算的频度 f(n) 来分析时间复杂度

Q:什么是 “基本运算”?

A:最坏和最好描述的运算对象

不仅与问题规模有关,还与代输入数据的性质(初始状态)有关,根据不同的初始状态,可将算法时间复杂度分为以下三类:

  • 最好时间复杂度
  • 平均时间复杂度
  • 最坏时间复杂度:一般考虑这种情况

常见复杂度比较:

O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(log_2n)<O(n)<O(nlog_2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

【2013】将两个长度分别为 m m m n n n 的升序链表合并成一个长度为 m + n m+n m+n 的降序列表,最坏时间复杂度为

A:O(max(m, n))

B:O(min(m, n))

B:合并算法:两条链表的元素两两比较,最坏的是两条链中的元素依次比较{2,3,4,5}和{1,6}

【p11】思考斐波那契递归算法(递推公式 F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n) = F(n-1) + F(n-2) F(n)=F(n1)+F(n2))的时间复杂度

O ( 2 n ) O(2^n) O(2n):可以想象成一个二叉树,每个父节点 F ( n ) F(n) F(n)都有两个子节点 F ( n − 1 ) F(n-1) F(n1) F ( n − 2 ) F(n-2) F(n2)

空间复杂度

算法原地工作:算法需要的辅助空间为常量O( 1 1 1)

考试时花费大量时间思考一个问题的最优解是得不偿失的, O ( n ) O(n) O(n)的算法能拿11分的话, O ( n 2 ) O(n^2) O(n2)的算法能拿10分

线性表

线性表是一种逻辑结构,其对应的存储结构有

  • 顺序表(数组)
  • 链表

顺序表(数组)

地址连续的存储单元

// 静态分配
#define MaxSize 50			//最大容量
typedef struct{
    Type data[MaxSize];
    int Length;   			//当前长度
} SqList

// 动态分配
#define InitSize 100	
typedef struct{
    Type *data;
    int MaxSize, Length;   	//最大容量 和 当前长度
} SeqList

插入

输入:

  • i(表示第 i 个位置插入)
  • e(要插入的新元素)

过程:

  • 先判断是否合法( i 有无越界,数组长度有无限制等)
  • 将第 i 个以及之后的所有元素后移一位
  • 第 i 个位置放入 e
  • Length += 1

删除

输入:

  • i(表示删除第 i 个元素)

过程:

  • 先判断是否合法( i 有无越界)
  • 将第 i 个以及之后的所有元素前移一位
  • Length -= 1

查找

输入:e(查找元素值等于 e 的元素

过程:

  • 遍历查找

【2010-循环左移】,一个序列 a 0 , a 1 , a 2 , … , a n a_0,a_1,a_2, … ,a_n a0,a1,a2,,an,要将此序列循环左移 p p p ( 0 < p < n ) (0<p<n) (0<p<n),得到 a p , a p + 1 , a p + 2 , … , a n , a 0 , a 1 , a 2 , … , a p − 1 a_{p}, a_ {p+1},a_{p+2}, … ,a_n, a_0, a_1, a_2, … ,a_{p-1} ap,ap+1,ap+2,,an,a0,a1,a2,,ap1 ,设计一个循环左移算法分析时间和空间复杂度

参考思路:将原串分成两个字串【0,p-1】和【p,n】,记将【1,2,3】反转为【3,2,1】的操作为Reverse,则 Reverse【Reverse【0,p-1】,Reverse【p,n】】即为循环左移的结果

void Reverse(int R[], int from, int to){
    int temp;
    while(from < to){
        temp = R[from];
        R[from] = R[to];
        R[to] = temp;
        from ++;
        to --;
    }
}

void Converse(int R[], int n, int p){
    Reverse(R, 0, p-1);
    Reverse(R, p, n-1);
    Reverse(R, 0, n-1);
}

时间复杂度为 O ( p / 2 ) + O ( ( n − p ) / 2 ) + O ( n / 2 ) = O ( n ) O(p/2)+O((n-p)/2)+O(n/2)=O(n) O(p/2)+O((np)/2)+O(n/2)=O(n)

空间复杂度为 O ( 1 ) O(1) O(1)

【2011-两个等长有序数组的中位数】即长度为 L 的数组中处在 L/2 位置的数称为“中位数”,如:

(11,13,15,17,19)中位数为 15

(2,4,6,8,11,13,15,17,19,20)中位数为 11

现有两个等长序列 A 和 B,设计一个算法找出 AB 合并后有序数列的“中位数”

参考思路:二分思维,记 A、B 的 “中位数“ 分别为 a、b,LA、RA分别为 A 被 a 分成的左右两部分,同理也有 LB、RB。

① 若 a=b ,则 a,b 都是所求的”中位数“

② 若 a<b ,则在 RA 和 LB 中找”中位数“

③ 若 a>b ,则在 LA 和 RB 中找”中位数“

参考代码:p25

时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)

空间复杂度为 O ( 1 ) O(1) O(1)

【扩展练习】两个不一定等长有序数组的中位数

链表

单链表

头结点:单链表的第一个节点前附加的节点,头结点数据域可以不设信息,也可以记录表长等相关信息

表头:如果带头结点的链表,头结点就是表头;不带头结点,表头就是第一个数据结点

头指针:指向表头

建立
  • 头插法:新节点插入到头结点之后,作为第一个数据结点
  • 尾插法:需要增加一个指向表尾的指针,每次新节点加到末尾,更新尾指针

双链表

比单链表多了一个前驱指针

循环链表

循环单链表:将单链表中最后一个结点的指针指向头节点,在操作尾结点时要注意维持这一 ”环“ 的特性

循环双链表:… …

静态链表

用数组实现的链表

数组下标0(头结点)1234
数据badc
下一结点的下标251-1(尾结点)3

栈和队列

栈是一种加了约束的线性表

栈顶(Top):允许出栈(删除)入栈(插入)的一端

栈底(Bottom):最先进来的,最后出去的那个崽

基本操作:

void InitStack(&S); // 初始化一个空栈
bool IsEmpty(S);	// 判断是否为空
void Push(&S, x);	// 若S未满,将元素x从栈顶放入
void Pop(&S, &x);	// 若S非空,将栈顶元素从栈顶拿出,并赋值给x
void GetTop(S, &x);	// 若S非空,读取栈顶元素,赋值给x
void Clear(&S);		// 销毁栈

顺序栈

用数组实现的栈,连续内存

#define MaxSize 50
typedef struct{
    Type data[MaxSize];
    int top;			// 栈顶指针,初始化或空栈时为-1,有内容时从0开始
} SqStack;

顺序栈内元素的个数 = S.top + 1

共享栈

两个栈在一块连续的内存,栈底分别在两端,栈顶向中间延申

#define MaxSize 50
typedef struct{
    Type data[MaxSize];
    int top0;			// 0号栈的栈顶指针,初始化或空栈时为 -1
    int top1;			// 1号栈的栈顶指针,初始化或空栈时为 MaxSize
} GayStacks;

当 top1 - top0 = 1 时,栈满了

链栈

采用无头结点的单链表实现,规定表头(第一个节点)为栈顶

【2013-进出栈分析】一个栈入栈先后顺序为 1 , 2 , 3 , 4 , … , n 1,2,3,4,… ,n 1234n,出栈先后顺序为 P 1 , P 2 , P 3 , P 4 , … , P n P_1,P_2,P_3,P_4,…,P_n P1,P2,P3,P4,,Pn,若 P 2 = 3 P_2=3 P2=3 ,则 P 3 P_3 P3 可能的取值有____种

显然,从4开始往后的 n − 3 n-3 n3 个数都行, P 1 = 1 P_1=1 P1=1 的话, P 3 P_3 P3 就可以为 2;同理 P 1 = 2 P_1=2 P1=2 的话, P 3 P_3 P3 就可以为 1。

所以答案是 n − 1 n-1 n1

队列

操作受限的线性表

队头:崽子们从这里出队

队尾:崽子们从这里入队

基本操作

void InitQueue(&Q); 	// 初始化一个空队
bool IsEmpty(Q);		// 判断是否为空
void EnQueue(&Q, x);	// 若Q未满,将元素x入队,成为队尾
void DeQueue(&Q, &x);	// 若Q非空,将队首的崽拎出来,并赋值给x
void GetHead(Q, &x);	// 若Q非空,读取队首元素,赋值给x

顺序队列

用数组实现的队列,连续存储

#define MaxSize 50
typedef struct{
    Type data[MaxSize];
    int front;			// 队头,初始化 = 0,有崽子出队时,先取值,再+1,
    int rear;			// 队尾,初始化 = 0,有崽子入队时,先赋值,再+1。
    					// 所以队尾处始终为 null
} SqQueue;

缺点是:数组空间用完一遍后,front 和 rear 都跑到 MaxSize 去了,出队时释放的空间不能再用了

改进👇

循环队列

将数组逻辑上视为一个”环“,当 rear 因为元素入队跑到 MaxSize - 1 时,再入队一个崽,rear就取余运算变成 0,重新回到存储空间的开始位置

入队赋值后:Q.rear = (Q.rear + 1) % MaxSize

出队取值后:Q.front = (Q.front + 1) % MaxSize

队列长度:Length = (Q.rear - Q.front + MaxSize) % MaxSize

真·队满和队空时都有 Q.front == Q.rear,所以仅凭判断首尾相等是区分不了队满和队空的,需要一些措施

  • 将 ”队满“ 定义为 ”假·队满“ 即 Q.front == (Q.rear + 1) % MaxSize 假·队满 : 未满,当队尾快要超圈,但还差一个单元时,就认为队列已满
  • 在循环队列的类中添加一个成员变量 size 记录崽子个数
  • 在循环队列的类中添加一个成员变量 tag 来记录Q.front == Q.rear的原因是删除还是增加

链式队列

一个单链表,头指针指向的是队头,尾指针指向的是队尾

typedef struct{
    Type data;
    struct Node *next;
} Node;

typedef struct{
    Node *front, *rear;  // 队列的队头和队尾,初始化的时候都指向头结点,如果两者相等,则队列为空
} LinkQueue

通常这个单链表会设置一个头结点,Q.front 指向这个头结点,出队的是第一个数据结点,即头结点的下一个Q.front->next 被拎出去

双端队列

两端都可以入队和出队

双端队列

给一个入队序列,要会判断哪些序列是可能的出对序列。

  • 输出受限的双端队列:两端都能入队,只有一端能出队
  • 输入受限的双端队列:两端都能出队,只有一端能入队

栈和队列的应用

括号匹配&表达式求值

表达式有以下三种不同记法

  • 中缀表达式:1 + (2 + 3) × 4 - 5
  • 前缀表达式:- + 1 × + 2 3 4 5
  • 后缀表达式:1 2 3 + 4 × + 5 -

人的大脑很容易理解与分析中缀表达式,但计算机更容易计算前缀或后缀表达式。

并且注意到只有中缀表达式是有括号来标志嵌套运算优先级的,前缀和后缀表达式并不需要括号就能处理。

所以计算表达式的值时,通常需要先将中缀表达式转换为前缀或后缀表达式,然后再进行求值。

【Q1】看看你的电脑如何处理后缀表达式:1 2 3 + 4 * +5 –

  1. 首先一个一个读入,是数字就压入栈(假设栈顶在右边,从右边入栈)中

1 2 3 // 读入了三个,全是数字,依次压栈

  1. 读到了一个运算符号 + ,关键点来了!

这个 + 号告诉你现在要做一个加法,那么哪两个数字相加呢?从栈里Pop出两个崽子啊!

于是你从栈里 Pop 出了 3 和 2,并做了一个加法 2 + 3 = 5 2+3=5 2+3=5 ,得到的结果再压回栈中

1 5 // 得到的 5 压回栈中,所以之前的 ‘2’,‘3’,’+’ 三个符号已经完成了它们的使命

  1. 再往下读到 4,压进去

1 5 4

  1. 再读到运算符号 * ,同样的操作方式,抓两个壮丁过来相乘,得到的结果压回去

1 20 // 5 × 4 = 20,20压回栈中

  1. 再一个运算符 +,咳咳

21 // 1 + 20 = 21,21压回栈中

  1. 一个数字 5,一个运算符 -

16 // 21 - 5 = 16

最终结果 16,

ps:这里最后一步的减法为什么是 21 - 5 而不是 5 - 21。再模拟一下读取到运算符(记为 o)的时候进行的操作:抓(从栈顶抓)两个壮丁,先抓到的叫A,后弹出的叫B,那么进行的运算是 B o A = Result,这个Result 压回栈

【Q2】如何进行各种缀的表达式之间的转换呢?

中缀 → 后缀

运算符优先级:

运算符(× ÷+ -)
进栈后优先级1536
进栈前(刚扫描)的优先级6421

算法:

  • 扫描到数字,直接添加到后缀表达式
  • 扫描到的运算符 a 的优先级 > 栈顶运算符 top 的优先级:a 进栈,扫描下一个
  • 否则弹出 top,并将 top 添加到后缀表达式
  • 然后再比较新的栈顶 newtop 和 a 的优先级,回到上两步

参考博客

以下是样例代码

#include <iostream>
#include <string>
#include <stack>
#include <algorithm>

using namespace std;

bool isOperator(char ch)
{
    string operators = "+-*/";
    return operators.find(ch) != operators.npos;
}

bool isOperand(char ch)
{
    if (ch >= '0' && ch <= '9')
    {
        return true;
    }
    else if (ch >= 'a' && ch <= 'z')
    {
        return true;
    }
    else if (ch >= 'A' && ch <= 'Z')
    {
        return true;
    }
    return false;
}

int calculate(char op, int x2, int x1)
{
    switch (op)
    {
    case '-':
        return (x2 - x1);

    case '+':
        return (x2 + x1);

    case '*':
        return (x2 * x1);

    case '/':
        return (x2 / x1);
    }
}

// 遍历栈
string stackToStr(stack<char> S)
{
    stack<char> temp = S;
    int i = 0;
    string stack = "";
    while (i < S.size())
    {
        stack += temp.top();
        temp.pop();
        i++;
    }
    reverse(stack.begin(), stack.end());
    return stack;
}

// 计算后缀表达式的值
int countPostfix(string postfix)
{
    stack<int> s;

    for (int i = 0; i < postfix.length(); i++)
    {
        if (postfix[i] >= '0' && postfix[i] <= '9')
            s.push(postfix[i] - '0');
        else
        {
            int x1 = s.top();
            s.pop();
            int x2 = s.top();
            s.pop();
            s.push(calculate(postfix[i], x2, x1));
        }
    }
    return s.top();
}

// 转换成后缀表达式
string toPostfix(string infix)
{
    // 存放运算符的栈
    stack<char> ops;
    // 后缀表达式的字符串
    string postfix = "";

    for (int i = 0; i < infix.length(); i++)
    {
        // cout << endl << "current char: " << infix[i] << endl;
        char c = infix[i];
        // 是数字,直接加在后缀表达式后面
        if (isOperand(c))
            postfix = postfix + c;

        // 是 右括号 ,把栈顶一直弹出,直到左括号
        else if (c == ')')
        {
            while (ops.top() != '(')
            {
                postfix = postfix + ops.top();
                ops.pop();
            }
            ops.pop();
        }

        // 遇到 +,- 因为是优先级最低的,所以把栈顶的一直弹出
        else if (c == '+' || c == '-')
        {
            while (ops.size() && isOperator(ops.top()))
            {
                postfix = postfix + ops.top();
                ops.pop();
            }
            // 弹出后,再把当前的 +,- 加到栈里
            ops.push(c);
        }

        // 遇到优先级高的运算符,直接加到栈里
        else
        {
            ops.push(c);
        }

        // cout << "postfix: " << postfix << endl;
        // cout << "stack: " << stackToStr(ops) << endl;
    }
    while (ops.size())
    {
        postfix = postfix + ops.top();
        ops.pop();
    }
    return postfix;
}

// 转换成前缀表达式
string toPrefix(string infix)
{
    reverse(infix.begin(), infix.end());
    stack<char> ops;
    string prefix = "";

    for (size_t i = 0; i < infix.length(); i++)
    {
        char c = infix[i];
        if (isOperand(c))
            prefix += c;
        else if (c == '(')
        {
            while (ops.top() != ')')
            {
                prefix += ops.top();
                ops.pop();
            }
            ops.pop();
        }
        else if (c == '+' || c == '-')
        {
            if (ops.size() && (ops.top() == '*' || ops.top() == '/'))
            {
                prefix += ops.top();
                ops.pop();
            }
            ops.push(c);
        }
        else
            ops.push(c);
    }
    while (ops.size())
    {
        prefix += ops.top();
        ops.pop();
    }

    reverse(prefix.begin(), prefix.end());
    return prefix;
}

// 计算前缀表达式
int countPrefix(string prefix)
{
    reverse(prefix.begin(), prefix.end());
    stack<int> nums;
    for (size_t i = 0; i < prefix.size(); i++)
    {
        char c = prefix[i];
        if (isOperand(c))
        {
            nums.push(c - '0');
        }
        else
        {
            int x1 = nums.top();
            nums.pop();
            int x2 = nums.top();
            nums.pop();
            nums.push(calculate(prefix[i], x1, x2));
        }
    }
    return nums.top();
}

int main()
{
    vector<string> expressions = {
            "6*(3*(4-3)+4)",
            "6-2-(5-6)+3-4",
            "1+(2+3)*(4-5)",
            "1+(2+3)*4-5",
            "1+2+3*4-5"
        };

    for (size_t i = 0; i < expressions.size(); i++)
    {
        string infix = expressions[i];
        cout << "expression: " << infix << endl;

        string postfix = toPostfix(infix);
        cout << "postfix: " << postfix << " ; result: " << countPostfix(postfix) << endl;

        string prefix = toPrefix(infix);
        cout << "prefix: " << prefix << " ; result: " << countPrefix(prefix) << endl;
        cout << endl;
    }

    return 0;
}

栈与递归

递归的精髓:将原始问题转化为属性相同但规模更小的问题【递归体】,然后给出一个边界条件【递归出口】解出问题

在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储。系统栈中保存的函数信息满足 先调用 先进 后出 的特点。

代码量少但效率不高,原因在于递归调用的过程中会包含对某一问题的重复求解【重复计算】

队列与层遍历

二叉树的层遍历

矩阵的压缩存储

多维数组: ( 数 组 的 ) n 数 组 (数组的)^n数组 n ,维度是一个逻辑上的,物理上的存储依然是一段连续的空间,拿二维数组来说,在物理存储上有两种映射方式

  • 按行优先:一行一行的 ∣ a 00 ∣ a 01 ∣ a 02 ∣ a 10 ∣ a 11 ∣ a 12 ∣ |a_{00}|a_{01}|a_{02}|a_{10}|a_{11}|a_{12}| a00a01a02a10a11a12
  • 按列优先:一列一列的 ∣ a 00 ∣ a 10 ∣ a 01 ∣ a 11 ∣ a 02 ∣ a 12 ∣ |a_{00}|a_{10}|a_{01}|a_{11}|a_{02}|a_{12}| a00a10a01a11a02a12

对称矩阵

只存一半(主对角线+上/下三角区域)

对于 n 阶方阵,只用一个一维数组 B[ n ( n + 1 ) / 2 n(n+1)/2 n(n+1)/2] 即可,映射方式为行优先

三角矩阵

不同于线性代数里的 “真·上下三角”(三角区域外全为0),这里的三角是指 “假·上下三角”(三角区域外全为一个常数C),因此要比上一个对称矩阵多出一项来放这个 C

三对角矩阵

长这样:【三条斜线】

在这里插入图片描述

行优先映射就完事儿了,只存这块斜带区域的值

稀疏矩阵

外形方面么得规律,但 0 元素超级多的矩阵

  • 用三元组(行,列,值)的数组保存
  • 十字链表

其它相关文章

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值