【数据结构】3. 栈和队列

目录

  • 3.1 栈

    • 3.1.1 栈的基本概念

      • (1)栈的定义
      • (2)栈的基本操作
    • 3.1.2 栈的顺序存储结构
      • (1)顺序栈的实现
      • (2)栈的基本运算
      • (3)共享栈
    • 3.1.3 栈的链式存储结构
  • 3.2 队列
    • 3.2.1 队列的基本概念

      • (1)队列的定义
      • (2)队列常见的基本操作
    • 3.2.2 队列的顺序存储结构
      • (1)队列的順序存储
      • (2)循环队列
      • (3)循环队列的操作
    • 3.2.3 队列的链式存储结构
      • (1)队列的链式存储
      • (2)链式队列的基本操作
    • 3.2.4 双端队列
  • 3.3 栈和队列的应用
    • 3.3.1 栈在括号匹配中的应用
    • 3.3.2 栈在表达式求值中的应用
    • 3.3.3 栈在递归中的应用
    • 3.3.4 队列在层次遍历中的应用
    • 3.3.5 队列在计算机系统中的应用
  • 3.4 特殊矩阵的压缩存储
    • 3.4.1 数组的定义
    • 3.4.2 数组的存储结构
    • 3.4.3 矩阵的压缩存储
      • (1)对称矩阵
      • (2)三角矩阵
      • (3)三对角矩阵
    • 3.4.4 稀疏矩阵

3.1 栈

3.1.1 栈的基本概念

(1)栈的定义

(Stack):只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但是限定这种线性表只能在某一端进行插入和删除操作,如图 3-1 所示。

栈顶(Top):线性表允许进行插入和删除的那一端。

栈底(Bottom):固定的,不允许进行插入和删除的另一端。

空栈:不含任何元素的空表。

假设某个栈 \(S=(a_1, a_2, a_3, a_4, a_5)\),如图 3-1 所示,则 \(a_1\) 为栈底元素,\(a_5\) 为栈顶元素。

由于栈只能在栈顶进行插入和删除操作,故进栈次序依次为 \(a_1\)、\(a_2\)、\(a_3\)、\(a_4\)、\(a_5\),而出栈次序为 \(a_5\)、\(a_4\)、\(a_3\)、\(a_2\)、\(a_1\)。

由此可见,栈的一个明显的操作特性可以概括为后进先出(Last In First Out, LIFO),故又称为后进先出的线性表。

注意:

我们每接触到一种新的数据结构类型,都应该分别从其逻辑结构、存储结构和对数据的运算三个方面着手,以加深对定义的理解。

(2)栈的基本操作

各种辅导书中给出的基本操作的名称不尽相同,但所表达的意思大致是一样的。

这里我们以严蔚敏编写的教材为准给出栈的基本操作,希望读者能熟记下面的基本操作:

  • InitStack(&S):初始化一个空栈 S。
  • StackEmpty(S):判断一个栈是否为空,若栈 S 为空返回 true,否则返回 false。
  • Push(&S, x):进栈,若栈 S 未满,将 x 加入使之成为新栈顶。
  • Pop(&S, &x):出栈,若栈 S 非空,弹出栈顶元素,并用 x 返回。
  • GetTop(S, &x):读栈顶元素,若找 S 非空,用 x 返回栈顶元素。
  • ClearStack(&S):销毁栈,并释放栈 S 占用的存储空间。

(注:符号“&”是 C++ 特有的,用来表示引用调用,有的书上采用 C 语言中的指针类型“*”,也可以达到传址的目的。)

在解答算法题时,若题干没有做出限制,可以直接使用这些基本的操作函数。

3.1.2 栈的顺序存储结构

(1)顺序栈的实现

栈的顺序存储称为顺序找,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶的位置。

栈的顺序存储类型可描述为

#define MaxSize 50 //定义找中元素的最大个数
typedef struct {
    Elemtype data[MaxSize]; //存放栈中元素
    int top; //栈顶指针
} SqStack;

栈顶指针:S.top,初始时设置 S.top = -1

栈顶元素:S.data[S.top]

进栈操作:栈不满时,栈顶指针先加 1,再送值到栈顶元素。

出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减 1。

栈空条件:S.top == -1

栈满条件:S.top == MaxSize-1;

栈长:S.top+1

由于顺序栈的入栈操作受数组上界的约束,当对栈的最大使用空间估计不足时,有可能发生栈上溢,此时应及时向用户报告消息,以便及时处理,避免出错。

对于栈和后面提到的队列的判空和判满条件会因为实际给的条件不同而变化,以上提到的方法以及下面给出的代码实现只是在栈顶指针设定的条件下相应的方法,而其他情况需要

具体问题具体分析。

(2)栈的基本运算

栈操作的示意图如图 3-2 所示,图 3-2(a) 是空栈,图 3-2(c) 是 A、B 、C、D、E 共 5 个元素依次入栈后的结果,图 3-2(d) 是在图 3-2(c) 之后 E、D、C 相继出栈,此时栈中还有 2 个元素,或许最近出找的元素 C、D、E 仍在原先的单元存储着,但 top 指针己经指向了新的栈顶,则元素 C、D、E 己不在栈中了,读者应通过该示意图深刻理解栈顶指针的作用。

下面是顺序栈上常用的基本运算的实现。

  1. 初始化

    void InitStack(&S) { s.top = -1; //初始化找顶指针 }

  2. 判栈空

    bool StackEmpty(S) { if(s.top == -1) //栈空 return true; else return false; //不空 }

  3. 进栈

    bool Push(SqStack &S, ElemType x) { if(S.top == MaxSize-1) //栈满,报错 return false; S.data[++S.top] = x; //指针先加 1,再入栈 return true; }

  4. 出栈

    bool Pop(SqStack &S, ElemType &x) { if(S.top == -1) //栈空,报错 return false; x = S.data[S.top--]; //先出栈,指针再减 1 return true; }

  5. 读栈顶元素

    bool GetTop(SqStack S, ElemType &x) { if(S.top == -1) //栈空,报错 return false; x = S.data[S.top]; //x 记录栈顶元索 return true; }

注意:

这里栈顶指针指向的就是栈顶元素,所以进栈时的操作是 S.data[++S.top] = x;出栈时的操作是 x = S.data[S.top--]

如果栈顶指针初始化为 S.top = 0,即栈顶指针指向栈顶元素的下一个位置,则入栈操作变为 S.data[S.top++] = x;出栈操作变为 x = S.data[--S.top]

相应的栈空、栈满条件也会发生变化。请读者仔细体会其中的不同之处,做題时也应灵活应变。

(3)共享栈

利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数据空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸,如图 3-3 所示。

两个栈的栈顶指针都指向栈顶元素,top0 = -1 时 0 号栈为空,top1 = MaxSize 时 1 号桟为空;

仅当两个栈顶指针相邻(top1-topO = 1)时,判断为栈满。

当 0 号栈进栈时 topO 先加 1 再赋值,1 号栈进栈时 top1 先减 1 再赋值;出栈时则刚好相反。

共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。

其存取数据的时间复杂度均为 \(\mathcal{O}(1)\),所以对存取效率没有什么影响。

3.1.3 栈的链式存储结构

采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。

通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。

这里规定链栈没有头结点,Lhead 指向栈顶元素,如图 3-4 所示。

栈的链式存储类型可描述为

typedef struct Linknode {
    ElemType data; //数据域
    struct Linknode *next;
} *LiStack; //指针域 栈类型定义

采用链式存储,便于结点的插入与删除。 链栈的操作与链表类似,在此不做详细讨论。

读者需要注意的是,对于带头结点和不带头结点的链栈,在具体的实现方面有所不同。

3.2 队列

3.2.1 队列的基本概念

(1)队列的定义

队列(Queue):队列简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表出队列一的另一端进行删除。

向队列中插入元素称为入队或进队:删除元素称为出队或离队。

这和我们日常生活中的排队是一致的,最早排队的也是最早离队的。

其操作的特性是先进先出(First In First Out, FIFO),故又称为先进先出的线性表,如图 3-5 所示。

队头(Front):允许删除的一端,又称为队首。

队尾(Rear):允许插入的一端。

空队列:不含任何元素的空表。

(2)队列常见的基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列 Q。
  • QueueEmpty(Q):判队列空,若队列 Q 为空返回 true, 否则返回 false。
  • EnQueue(&Q, x):入队, 若队列 Q 未满,将 x 加入,使之成为新的队尾。
  • DeQueue(&Q, &x):出队,若队列 Q 非空,删除队头元素,并用 x 返回。
  • GetHead (Q, &x):读队头元素,若队列 Q 非空,则将队头元素赋值给 x。

需要注意的是,队列是操作受限的线性表,所以,不是任何对线性表的操作都可以作为队列的操作。

比如,不可以随便读取队列中间的某个数据。

3.2.2 队列的顺序存储结构

(1)队列的順序存储

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针 front 和 rear 分别指示队头元素和队尾元素的位置。

设队头指针指向队头元素,队尾指针指向队尾元素的下一个位置(也可以让 rear 指向队尾元素,front 指向队头元素的前一个位罝,对于这种设置方法,请读者以图 3-6 为例思考出队和入队后这两个指针的变化)。

队列的顺序存储类型可描述为:

#define MaxSize 50
typedef struct {
    ElemType data[MaxSize];
    int front,rear;
} SqQueue;

初始状态(队空条件):Q.ftont == Q.rear==0

进队操作:队不满时,先送值到队尾元素,再将队尾指针加 1。

出队操作:队不空时,先取队头元素值,再将队头指针加 1。

如图 3-6(a) 所示为队列的初始状态,有 Q.front == Q.rear == 0 成立,该条件可以作为队列判空的条件。

但能否用 Q.rear == MaxSize 作为队列满的条件呢?

显然不能,图 34(d) 中,队列中仅有 1 个元素,但仍满足该条件。

这时入队出现 “上溢出”,但这种溢出并不是真正的溢出,在 data 数组中依然存在可以存放元素的空位置,所以是一种“假溢出”。

(2)循环队列

前面已指出了顺序队列的缺点,这里我们引出循环队列的概念。

将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上看成一个环,称为循环队列。

当队首指针 Q.front=MaxSize-1 后,再前进一个位置就自动到 0, 这可以利用除法取余运算(%)来实现。

初始时:Q.front = Q.rear = 0

队首指针进 1:Q.front = (Q.front+1) % MaxSize

队尾指针进 1:Q.rear = (Q.rear+1) % MaxSize

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

出队入队时:指针都按顺时针方向进 1(如图 3-7 所示)。

那么,循环队列队空和队满的判断条件是什么呢?

显然,队空的条件是 Q.front == Q.rear

如果入队元素的速度快于出队元素的速度,队尾指针很快就赶上了队首指针,如图 3-7(d1) 所示,此时可以看出队满时也有 Q.front == Q.rear

循环队列出入队示意图如图 3-7 所示。

为了区分队空还是队满的情况,有三种处理方式:

  1. 牺牲一个单元来区分队空和队满,入队时少用一个队列单元,这是一种较为普遍的做法,约定以“队头指针在队尾指针的下一位置作为队满的标志”,如图 3-7(d2) 所示。

    队满条件为:(Q.rear+1) % MaxSize == Q.front

    队空条件为:Q.front == Q.rear

    队列中元素的个数:(Q.rear-Q.front+MaxSize) % MaxSize

  2. 类型中增设表示元素个数的数据成员。

    这样,则队空的条件为 Q.size == 0;队满的条件为 Q.size == MaxSize

    这两种情况都有 Q.front == Q.rear

  3. 类型中增设 tag 数据成员,以区分是队满还是队空。

    tag 等于 0 的情况下,若因删除导致 Q.front == Q.rear 则为队空;

    tag 等于 1 的情况下,若因插入导致 Q.front == Q.rear 则为队满。

(3)循环队列的操作

  1. 初始化

    void InitQueue(&Q) { Q.rear = Q.front = 0; //初始化队首、 队M指针 }

  2. 判队空

    bool isEmpty(Q) { if(Q.rear==Q.front) //队空条件 return true; else return false; }

  3. 入队

    bool EnQueue(SqQueue &Q, ElemType x) { if((Q.rear+1)%MaxSize == Q.front) //队满 return false; Q.data[Q.rear] = x; Q?rear = (Q.rear+1) % MaxSize; //队尾指针加 1 取模 return true; }

  4. 出队

    bool DeQueue(SqQueue &Q, ElemType &x) { if(Q.rear==Q.front) //队空,报错 return false; x = Q.data[Q.front]; Q.front = (Q.front+1) % MaxSize; //队头指针加 1 取模 return true; }

3.2.3 队列的链式存储结构

(1)队列的链式存储

队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。

头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点(注意与顺序存储的不同)。

队列的链式存储如图 3-8 所示:

队列的链式存储类型可描述为:

typedef struct { //链式队列结点
    ElemType data;
    struct LinkNode *next;
} LinkNode;
typedef struct { //链式队列
    LinkNode *front, *rear; //队列的队头和队尾指针
} LinkQueue;

Q.front==NULLQ.rear==NULL 时, 链式队列为空。

出队时,首先判断队是否为空,若不空,则取出队头元素,将其从链表中摘除,并让 Q.front 指向下一个结点(若该结点为最后一个结点,则置 Q.frontQ.rear 都为 NULL)。

入队时,建立一个新结点,将新结点插入到链表的尾部,并改让 Q.rear 指向这个新插入的结点(若原队列为空队,则令 Q.front 也指向该结点)。

不难看出,不设头结点的链式队列在操作上往往比较麻烦,因此,通常将链式队列设计成一个带头结点的单链表,这样插入和删除操作就统一了,如图 3-9 所示。

用单链表表示的链式队列特别适合于数据元素变动比较大的情形,而且不存在队列满且产生溢出的问题。

另外,假如程序中要使用多个队列,与多个栈的情形一样,最好使用链式队列,这样就不会出现存储分配不合理和“溢出”的问题。

(2)链式队列的基本操作

  1. 初始化

    ```

    void InitQueue(LinkQueue &Q) {

    Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode)); //建立头结点

    Q.front->next = NULL; //初始为空

    }

  2. 判队空

    bool IsEmpty(LinkQueue Q) { if(Q.front == Q.rear) return true; else return false; }

  3. 入队

    void EnQueue(LinkQueue &Q, ElemType x) { s = (LinkNode*)malloc(sizeof(LinkNode)); s->data = x; s->next = NULL; //创建新结点,插入到链尾 Q.rear->next = s; Q.rear = s; }

  4. 出队

    bool DeQueue(LinkQueue &Q, ElemType &x) { if(Q.front==Q.rear) return false; //空队 p = Q.front->next; x = p->data; Q.front->next = p->next; if(Q.rear==p) Q.rear = Q.front;//若原队列中只有一个结点,删除后变空 free(p); return true; }

3.2.4 双端队列

双端队列是指允许两端都可以进行入队和出队操作的队列,其元素的逻辑结构仍是线性结构,如图 3-10 所示。

将队列的两端分别称为前端和后端,两端都可以入队和出队 。

在双端队列进队时:前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。

在双端队列出队时:无论前端还是后端出队,先出的元素排列在后出的元素的前面。

思考:如何由入队序列 a,b,c,d 得到出队序列 d,c,a,b?

输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列,如图 3-11 所示。

输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输入受限的双端队列,如图 3-12 所示。

而如果限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈了。

3.3 栈和队列的应用

要熟练掌握栈和队列,必须学习栈和队列的应用,把握其中的规律,然后才能举一反三。接

下来将简单介绍栈和队列的一些常见应用。

3.3.1 栈在括号匹配中的应用

假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序任意,即 ([]())[([][])]等均为正确的格式,

[(])([())(()]均为不正确的格式。

考虑下列括号序列:

[ ( [ ] [ ] ) ]

1 2 3 4 5 6 7 8

分析如下:

  1. 计算机接收第 1 个括号 [ 后,期待与之匹配的第 8 个括号 ] 出现。
  2. 获得了第 2 个括号 (,此时第 1 个括号[暂时放在一边,而急迫期待与之匹配的第 7 个括号)的出现。
  3. 获得了第 3 个括号[,此时第 2 个括号 ( 暂时放在一边,而急迫期待与之匹配的第 4 个括号的出现。

    第 3 个括号的期待得到满足,消解之后,第 2 个括号的期待匹配又成为当前最急迫的任务。

  4. 依此类推,可见,该处理过程与栈的思想吻合。

算法的思想如下:

  1. 初始设置一个空栈,顺序读入括号。
  2. 若是右括号,则或者使置于栈顶的最急迫期待得以消解,或者是不合法的情况(括号序列不匹配,退出程序)。
  3. 若是左括号,则作为一个新的更急迫的期待压入找中,自然使原有的在栈中的所有未消解的期待的急迫性降了一级。

    算法结束时,栈为空,否则括号序列不匹配。

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

表达式求值是程序设计语言编译中一个最基本的问题,它的实现是栈应用的一个典型范例。

中缀表达式不仅依赖运算符的优先级,而且还要处理括号。

后缀表达式的运算符在操作数后面,在后缀表达式中己考虑了运算符的优先级,没有括号,只有操作数和运算符。

中缀表达式 A+B*(C-D)-E/F 所对应的后缀表达式为 ABCD-*+EF/

中缀表达式转化为后缀表达式的过程,见本章习题 3.3.6 第 11 题的解析,这里不再赘述。

读者也可以将后缀表达式与原运算式对应的表达式树(用来表示算术表达式的二元树,如图 3-15)的后序遍历进行比较,可以发现它们有异曲冋工之妙。

通过后缀表示计算表达式值的过程为:

顺序扫描表达式的每一项,然后根据它的类型做如下相应操作:

如果该项是操作数,则将其压入栈中;

如果该项是操作符

3.3.3 栈在递归中的应用

递归是一种重要的程序设计方法。

简单地说,如果在一个函数、过程或数据结构的定义中又应用了它自身,那么这个函数、过程或数据结构称为是递归定义的,简称递归。

它通常把一个大型的复杂问题,层层转化为一个与原问题相似的规模较小的问题来求解。

递归策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

但在通常情况下,它的效率并不是太髙。

以斐波那契数列为例,其定义为

\[fib(n)= \begin{cases} fib(n-1)+fib(n-2) &\, n>1 \\ 1 &\, n=1 \\ 0 &\, n=0 \end{cases}\]

这就是递归的一个典型例子,用程序来实现如下:

int Fib(n){ //斐波那契数列的实现
    if(n==0)
        return 0; //边界条件
    else if(n==1)
        return 1; //边界条件
    else
        return Fib(n-1)+Fib(n-2); //递归表达式
}

必须注意递归模型不能是循环定义的,其必须满足下面的两个条件:

  • 递归表达式(递归体)。
  • 边界条件(递归出口)。

递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题。

在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成桟溢出等。

而其效率不高的原因是递归调用过程中包含很多重复的计算

下面以 n=5 为例,列出递归调用执行过程,如图 3-16 所示。

显然,在递归调用的过程中,Fib(3) 被计算了 2 次,Fib(2)被计算了 3 次。

Fib(1) 被调用了 5 次,Fib(0) 被调用了 3 次。

所以,递归的效率低下,但优点是代码简单,容易理解。

在第 4 章的树中利用了递归的思想,代码将会变得十分简单。

通常情况下,初学者对于递归的调用过程很难理解,若读者想具体了解递归是如何实现的,可以参阅《编译原理》 的相关内容。

可以将递归算法转换为非递归算法,通常潘要借助栈来实现这种转换。

3.3.4 队列在层次遍历中的应用

在信息处理中有一大类问题需要逐层或逐行处理。

这类问题的解决方法往往是在处理当前层或当前行时就对下一层或下一行做预处理,把处理顺序安排好,待当前层或当前行处理完毕,就

可以处理下一层或下一行。

使用队列是为了保存下一步的处理顺序。

下面用二叉树(见图 3-17)层次遍历的例子,说明队列的应用。

表 3-2 显示了层次遍历二叉树的过程。

该过程的简单描述如下:

  1. 根结点入队。
  2. 若队空(所有结点都已处理完毕),则结束遍历;否则重复 3 操作。
  3. 队列中第一个结点出队,并访问之。

    若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回 2。

3.3.5 队列在计算机系统中的应用

队列在计算机系统中的应用非常广泛,以下仅从两个方面来简述队列在计算机系统中的作用:

第一个方面是解决主机与外部设备之间速度不匹配的问题,

第二个方面是解决由多用户引起的资源竞争问题。

对于第一个方面,仅以主机和打印机之间速度不匹配的问题为例作简要说明。

主机输出数据给打印机打印,输出数据的速度比打印数据的速度要快得多,由于速度不匹配,若直接把输出的数据送给打印机打印显然是不行的。

解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入到这个缓冲区中,写满后就暂停输出,转去做其他的事情。

打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。

主机接到请求后再向缓冲区写入打印数据。

这样做既保证了打印数据的正确,又使主机提高了效率。

由此可见,打印数据缓冲区中所存储的数据就是一个队列。

对于第二个方面,CPU(即中央处理器,它包括运算器和控制器)资源的竞争就是一个典型的例子。

在一个带有多终端的计算机系统上,有多个用户需要 CPU 各自运行自己的程序,它们分别通过各自的终端向操作系统提出占用 CPU 的请求。

操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把 CPU 分配给队首请求的用户使用。

当相应的程序运行结束或用完规定的时间间隔后,则令其出队,再把 CPU 分配给新的队首请求的用户使用。

这样既满足了每个用户的请求,又使 CPU 能够正常运行。

3.4 特殊矩阵的压缩存储

矩阵在计算机图形学、 工程计算中占有举足轻重的地位。

在数据结构中考虑的是如何用最小的内存空间来存储同样的一组数据。

此处我们无需研究矩阵及其运算等,而是专注于研究如何将矩阵更有效地存储在内存中,并能方便地提取矩阵中的元素。

3.4.1 数组的定义

数组是由 n(\(n\gt 1\))个相同类型的数据元素构成的有限序列,

每个数据元素称为一个数组元素,每个元素受 n 个线性关系的约束,

每个元素在 n 个线性关系中的序号称为该元素的下标,并称该数组为 n 维数组。

数组与线性表的关系:数组是线性表的推广。

一维数组可以看做是一个线性表;二维数组可以看做元素是线性表的线性表,依此类推。

数组一旦被定义,它的维数和维界就不再改变。

因此,除了结构的初始化和销毁之外,数组只会有存取元素和修改元素的操作。

3.4.2 数组的存储结构

大多数计算机语言都提供了数组数据类型,逻辑意义上的数组可以采用计算机语言中的数组数据类型进行存储,一个数组的所有元素在内存中占用一段连续的存储空间。

以一维数组 A[n] 为例,其存储结构关系式为

\[LOC(a_i) = LOC(a_0) + (i)\times L \, (0\le i\lt n)\]

其中,L 是每个数组元素所占存储单元。

对于多维数组,有两种映射方法:按行优先和按列优先。

以二维数组为例,按行优先存储的基本思想是:先行后列,先存储行号较小的元素,行号相等先存储列号较小的元素。

设二维数组的行下标与列下标的范围分别为 \([l_1, h_1]\) 与 \([l_2, h_2]\),则存储结构关系式为

\[LOC(a_{i,j}) = LOC(a_{l_1,l_2}) + [(i-l_1) * (h_2-l_2+1)+(j-l_2)] * L\]

假设 \(l_1\) 和 \(l_2\) 的值均为 0,则上式变为

\[LOC(a_{i,j})= LOC(a_{0,0}) + [i\times (h_2 +1)+j]* L\]

例如,对于数组 \(A_{2\times 3}\), 它按行优先方式在内存中的存储形式如图 3-19 所示。

当以列优先方式存储时,得出存储结构关系式为

\[LOC(a_{i,j}) = LOC(a_{l_1,l_2}) + [(j-l_2)*(h_1-l_1+1)+(i-l_1)]* L\]

例如,对于数组 \(A_{2\times 3}\),它按列优先方式在内存中的存储形式如图 3-20 所示。

3.4.3 矩阵的压缩存储

压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省存储空间。

特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。

常见的特殊矩阵有对称矩阵、 上(下)二角矩阵、 对角矩阵等。

特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的值相同的多个矩阵元素压缩存储到一个存储空间中。

(1)对称矩阵

若一个 n 阶方阵 A[n][n] 中的任一个元素 \(a_{i.j}\),都有 \(a_{i,j}=a_{j,i}\)(\(1\le i,j\le n\)),则称其为对称矩阵。

对于一个 n 阶方阵,其中元素可以划分为 3 个部分,即上三角区、 主对角线和下三角区,如图 3-21 所示。

对于 n 阶对称矩阵,上三角区所有元素和下三 角区对应元素相同,如果还采用二维数组存放,就会浪费几乎一半的空间,

为此将对称矩阵 A[n][n]存放在一维数组 B[n(n+1)/2]中,即元素 \(a_{i,j}\) 存放在 \(b_k\) 中。

只存放主对角线和下三角区的元素。

在数组 B 中,位于元素 \(a_{i,j}\)(\(i\ge j\)) 前面的元素个数为

第 1 行:1 个元素(\(a_{1,1}\));

第 2 行:2 个元素 (\(a_{2,1}, a_{2,2}\));

……

第 i-1 行:i-1 个元素(\(a_{i-1,1}, a_{i-1,2},\cdots, a_{i-1,i-1}\))

第 i 行:j-1 个元素(\(a_{i,1}, a_{i,2}, a_{i,j-1}\))

故,元素 \(a_{i,j}\) 在数组 B 中的下标 \(k= 1+2+\cdots +(i-1) + j-1 = (i-1)i/2 + j-1\)(数组下标从 0 开始)。

因此,元素下标之间的对应关系如下:

\[k = \begin{cases} \frac{i(i-1)}{2} +j-1 &\, i\ge j\text{(下三角区和主对角线元素)} \\ \frac{j(j-1)}{2} + i-1 &\, i\lt j\text{(上三角区元素)} \end{cases}\]

当数组下标从 1 开始时,可以采用同样的推导方法,请读者自行思考。

(2)三角矩阵

下三角矩阵(见图 3-23(a))中,上三角区的所有元素均为同一常量。

其存储思想与对称矩阵类似,不同之处在于存储完下三角区和主对角线上的元素之后,紧接着存储对角线上方的常量一次,

故可以将下三角矩阵 A[n][n] 压缩存储在 B[n(n+1)/2+1]中。

元素下标之间的对应关系为

\[k = \begin{cases} \frac{i(i-1)}{2} +j-1 &\, i\ge j\text{(下三角区和主对角线元素)} \\ \frac{n(n+1))}{2} &\, i\lt j\text{(上三角区元素)} \end{cases}\]

下三角矩阵的在内存中的压缩存储形式如图 3-22 所示。

上三角矩阵(见图 3-23(b))中,下三角区的所有元素均为同一常量。

只需存储主对角线、上三角区上的元素和下三角区的常量一次,可以将其压缩存储在 B[n(n+1)/2+1] 中。

在数组 B 中,位于元素 \(a_{i,j}\)(\(i\le j\))前面的元素个数

第 1 行:n 个元素

第 2 行:n-1 个元素

……

第 i-1 行:n-i+2个元素

第 i 行:j-i 个元素

故,元素 \(a_{i,j}\) 在数组 B 中的下标 \(k = n+(n-1)+\cdots+(n-i+2)+(j-i+1)-1 = (i-1)(2n-i+2)/2+(j-i)\)。

因此,元素下标之间的对应关系如下:

\[k = \begin{cases} \frac{(i-1)(2n-i+2)}{2} +(j-i) &\, i\le j\text{(上三角区和主对角线元素)} \\ \frac{n(n+1)}{2} &\, i\gt j\text{(下三角区元素)} \end{cases}\]

上三角矩阵的在内存中的压缩存储形式如图 3-24 所示。

以上推导均假设数组的下标是从 0 开始,若題设有具体要求,则应该灵活应对。

(3)三对角矩阵

对角矩阵也称为带状矩阵。

对于 n 阶方阵 A 中的任一元

素 \(a_{i,j}\),当 \(|i-j|\gt 1\) 时,有 \(a_{i,j}=0\)(\(1\le i,j\le n\)),则称为三对角矩阵,如图 3-25 所示。

在三对角矩阵中,所有非零元素都集中在以主对角线为中心的 3 条对角线的区域中,其他区域的元素都为零。

三对角矩阵 A 也可以采用压缩存储,将 3 条对角线上的元素按行优先方式存放在一维数组 B 中,且 \(a_{1,1}\) 存放于 B[0] 中,其存储形式如图 3-26 所示。

由此可以计算矩阵 A 中 3 条对角线上的元素 \(a_{i,j}\)(\(1\le i,j \le n,\, |i-j|\le 1\)) 在一维数组 B 中存放的下标为 k=2i+j-3。

反之,若已知三对角线矩阵中某元素 \(a_{i,j}\) 在一维数组 B 中存放于第 k 个位罝,则可求得

\(i=\lceil(k+1)/3+1\rceil, j=k-2i+3\)。

例如,当 k=0 时,\(i= \lceil(0+1)/3+1\rceil =1, j=0-2\times 1+3=1\), 存放的是 \(a_{i,1}\);

当 k=2 时,\(i=\lceil(2+1)/3+1\rceil=2, j=2-2\times 2+3=1\),存放的是 \(a_{2,1}\);

当 k=4 时,\(i=\lceil(4+1)/3+1\rceil=2, j=4-2\times 2+3=3\),存放的是 \(a_{2,3}\)。

3.4.4 稀疏矩阵

矩阵元素个数 s 相对于矩阵中非零元素个数 t 来说非常多,即 \(s\gg t\) 的矩阵称为稀疏矩阵。

例如,若一个矩阵的阶为 \(100\times 100\),而该矩阵中只有少于 100 个非零元素。

如果采用常规的方法存储稀疏矩阵,那将相当浪费存储空间,因此仅存储非零元素。

但通常零元素的分布没有规律,所以,仅存储非零元素的值是不够的,还要存储它所在的行和列。

因此,将非零元素及其相应的行和列构成一个三元组(行标,列标,值),如图 3-27 所示,然后再按照某种规律存储这些二元组。

稀疏矩阵压缩存储后便失去了随机存取特性。

原文地址:https://www.cnblogs.com/4thirteen2one/p/9388547.html

时间: 2024-11-08 16:19:10

【数据结构】3. 栈和队列的相关文章

【数据结构】栈和队列

栈和队列 容器数据结构是指一些包含了若干个其他相同或不同的数据结构的数据结构,被包含的这些每一个独立的数据结构都被称为一个元素,在一个容器中的元素往往支持相同的操作,具有类似的性质.之前说到过的线性表其实就是一种容器数据结构,本文中介绍的两种最常用的容器数据结构是栈和队列. 从功能上看,栈和队列大多用于计算过程中保存临时数据,这些数据是在计算过程中发现或产生的.在而后的计算中可能会用到这些数据.如果这些数据是固定的个数以及大小的话,可以构建几个变量来储存它们,但是如果这些数据不确定的话,就需要一

二、数据结构之栈、队列、循环队列

二.数据结构之栈.队列.循环队列 顺序栈 Stack.h 结构类型,函数声明: #ifndef _STACK_H_ #define _STACK_H_ typedef int SElementType; ///顺序栈 #define STACK_INIT_SIZE 20 #define STACK_INCREMENT 10 typedef struct { SElementType * base; SElementType * top; int stackSize;///当前栈的大小 }SqSt

数据结构之栈和队列

数据结构学习继续向前推进,之前对线性表进行了学习,现在我们进入栈和队列的学习.同样我们先学习一些基本概念以及堆栈的ADT. 栈和队列是两种中重要的线性结构.从数据结构角度看,栈和队列也是线性表,只不过是受限的线性表.因此可以称为限定性数据结构.但从数据类型来看,他们是和线性表大不相同的两类重要的抽象数据类型. 栈:(stack)是限定仅在表尾进行相应插入和删除操作的线性表.因此,对栈来说,表尾有其特殊含义,称为栈顶,表头称为栈底,不含元素的空表称为空栈.栈一个重要特性就是后进先出.OK,我们来看

数据结构之栈与队列

数据结构的有一个重要结构栈,栈这种数据结构就是满足先进后出的这种规则的数据结构就是栈,引用<大话数据结构>中的一个形象例子就是,子弹的弹夹,最先压入弹夹的子弹最后一个出弹夹,正好往一个栈里添加一个元素叫压栈.入栈,从栈里出来一个元素叫弹栈,出栈.指示器就叫栈帧. 栈图 现在就贴上代码: 栈的几个基本操作: #include<stdio.h> #include<stdlib.h> #include<string.h> typedef struct node{

数组拷贝、数组函数、通过数组函数来模拟数据结构的栈和队列、回调的意义、数组函数的排序问题、算法以及寻找素数的筛选法

1.数组的拷贝数组拷贝时指针的指向问题. 数组在拷贝时,指针的位置随之复制[这一点拷贝是完全一样]但是如果拷贝的数组的指针是非法的,那么拷贝出新指针的位置进行初始化<?php$arr1=array('123');end($arr1);next($arr1);//这个指针非法$arr2 = $arr1;//这里进行数组的拷贝var_dump(current($arr2));//得到指向‘123’元素的指针var_dump(current($arr1));//此时这个数组的指针有问题?> 但是拷贝

[ACM训练] 算法初级 之 数据结构 之 栈stack+队列queue (基础+进阶+POJ 2442+1442)

再次面对像栈和队列这样的相当基础的数据结构的学习,应该从多个方面,多维度去学习. 首先,这两个数据结构都是比较常用的,在标准库中都有对应的结构能够直接使用,所以第一个阶段应该是先学习直接来使用,下一个阶段再去探究具体的实现,以及对基本结构的改造! C++标准库中 这里记录一个经典的关于栈和队列的面试题目: 题目:实现一个栈,带有出栈(pop),入栈(push),取最小元素(getMin)三个方法.要保证这三个方法的时间复杂度都是O(1). 思路:重点是getMin()函数的设计,普通思路是设计一

浅谈数据结构系列 栈和队列

计算机程序离不开算法和数据结构,在数据结构算法应用中,栈和队列应用你比较广泛,因为两者在数据存放和读取方面效率比较高,本章节重点讲解两者的基本概念和实现. 基本概念 栈:是一种先进后出,后进先出的数据结构,本质上是线性表,只是限制仅允许在表的一段进行插入和删除工作.此端为栈顶,这是在栈中应用很关键的概念.所有数据的处理都是在栈顶进行的,进栈时,栈中元素增加,栈顶上移一位,出栈时栈顶下移一位.应用中比如:洗碗,每次洗干净的碗放在上面-进栈,取碗,从顶上取出一个-出栈:装子弹-进栈,开枪-出栈. 队

数据结构入门——栈与队列

栈与队列是两种重要的数据结构,有着广泛的应用,他们可以通过对链表功能加以限制改造而来.栈是一种先进后出(FILO)的数据结构,只能在一头进行加入删除,而队列是一种先进先出(FIFO)的数据结构,一头只能加入,另一头只能删除. 栈的实现: # include <stdio.h> # include <malloc.h> # include <stdlib.h> typedef struct Node { int data; struct Node * pNext; }NO

python——python数据结构之栈、队列的实现

这个在官网中list支持,有实现. 补充一下栈,队列的特性: 1.栈(stacks)是一种只能通过访问其一端来实现数据存储与检索的线性数据结构,具有后进先出(last in first out,LIFO)的特征 2.队列(queue)是一种具有先进先出特征的线性数据结构,元素的增加只能在一端进行,元素的删除只能在另一端进行.能够增加元素的队列一端称为队尾,可以删除元素的队列一端则称为队首. 地址在 http://docs.python.org/2/tutorial/datastructures.

数据结构之栈和队列及其Java实现

栈和队列是数据结构中非常常见又非常基础的线性表,在某些场合栈和队列使用很多,因此本篇主要介绍栈和队列,并用Java实现基本的栈和队列,同时用两个栈实现队列和用两个队列实现栈. 栈:栈是一种基于"后进先出"策略的线性表.在插入时(入栈),最先插入的元素在栈尾,最后插入的元素在栈顶:在删除时(出栈),最后插入的元素先出栈,最先插入的元素最后出栈.由此可见,对栈的插入和删除操作都是在栈顶位置进行的. 在Java中,提供了一个类Stack<E>来实现栈的这些特性,并提供了一些常用的