2. 湖南大学 数据结构 线性表

重回大二–数据结构–线性表

0. 教材

电子工业出版社 数据结构算法与分析第三版 Clifford A.Shaffer

1. 定义

元素的数据项组成的一种有限且有序的序列。是一个具有n个数据元素的有限序列。

除第一个和最后一个元素外,其余元素都有且仅有一个直接前驱和直接后驱

书上用抽象类表示ADT 如下,注意任何继承该类实现的线性表都必须实现这些,否则会报错

#ifndef LIST_H
#define LIST_H
#include <iostream>
using namespace std;
namespace bisheng{
template <typename E>
class List
{
private:
    void operator =(const List&) {}
    List(const List&) {}
public:
    List(){}//default constructor
    virtual ~List(){}
    virtual void lclear()=0;
    virtual void lInsert(const E& item)=0;
    virtual void lappend(const E& item)=0;
    virtual void lprev()=0;
    virtual void lnext()=0;
    virtual E lremove()=0;
    virtual void lmoveToStart()=0;
    virtual void lmoveToEnd()=0;
    virtual void lmoveToPos(int pos)=0;
    virtual int lgetLength()const=0;
    virtual int lcurrentPos() const =0;
    virtual const E& lgetValue() const =0;
    virtual void lprint()  = 0;
};
}
#endif // LIST_H

题目

线性表的逻辑结构是:线性结构 其所含元素的个数称为线性表的长度

2. 两种实现方式

1. 基于数组

具体实现网上一抓一大把,粘贴我的代码上来显得复习提纲不够精简。所以直接上特点

特点:

  1. 逻辑上相邻的元素,在物理中一定相邻
  2. 先声明数组长度,因而可能存在空间浪费,造成存储空间的碎片
  3. insert/remove时间开销θ(n),移动次数为n/2
  4. 快速获取某个位置元素或者其前驱后继
  5. 线性表的顺序存储结构是一种随机存取的存储结构,因为可以通过计算公式随机地取某个位置的元素. 获取第i个数据元素ai的存储位置:LOC(ai)=LOC(a1)+(i-1)*sizeof(ai)

2.基于链表

链表是一种用于存储数据集合的数据结构。链表中相邻元素之间通过指针链接,最后一个元素的后继指针为NULL(循环链表除外)

2.0 特点
  1. 用一组任意的存储单元存储线性表的数据元素
  2. 利用指针实现了用不相邻的存储单元存储逻辑上相邻的元素(我感觉和1无差别)
  3. 空间按需分配,无内存空间的浪费。但每个数据元素ai除存储本身信息外还有结构开销
  4. 访问某个位置或其元素的时间开销为θ(n)
  5. insert remove时间开销为θ(1)
2.1 单链表
2.1.1 非循环

有头结点的/无头结点的情况
head

  • 头结点:在单链表的第一个结点前附设一的一个结点,作用是为更方便的操作链表。不一定是链表必须要素;
  • 头指针head:是指链表指向第一个结点的指针,若链表有头结点,则是指向头节点的指针。无论链表是否为空,头指针均不为空,头指针是链表的必要元素;
  • 最后一个结点:指针指向Null

题目:

  1. 不带头结点的单链表head 为空判定条件是:head==NULL
  2. 带头结点的单链表head为空条件:head->next==NULL
  3. 单链表不是一种随机存取结构 ☑️
2.1.2 循环单链表

带头指针的单循环链表

为空时

头指针空链表

非空时:

单链表循环

缺点:用作队列时,在队列尾部添加元素时间复杂度高,需要遍历整个线性表,因此实现队列常用带尾指针的单循环链表。

题目:

  1. 循环链表的主要优点是:在表中从任意位置出发都能扫描整个链表

  2. 在一个以 h 为头指针的单循环链中,p 指针指向链尾结在这里插入代码片点的条件是()
    A)p->next == NULL
    B) p->next == h
    C)p->next->next == h
    D) p->data == -1

  3. 算法设计题:设计一个算法将以链表实现单向链表按照elem的值从小到大排序
    myanswer

void Lsort()
{
 node<E> *first, *curr, *nxt,*left,*right;
 first = head->next;
 curr = first->next;
 if(!curr) return ;//一个元素就别排了
 head->next->next = NULL;
 while(curr)
 {
     // 从第二个元素开始往后遍历
     // 第一个循环中  curr 为第二个元素, nxt为curr的后一个
     // first永远是第一个
     nxt=curr->next;
     first=head->next;
     if (curr->elem <= first->elem)
     {
         curr->next = first;
         head->next = curr;
     }
     else
     {
         // 否则 找到一个比 currr 大的位置 left,right, 把 r 插在 left 的后一个  right前一个
         left=first;
         right=first->next;
         while(right&& ((right->elem) < (curr->elem)))
         {
             left=right;
             right=right->next;
         }
         left->next=curr;
         curr->next=right;
     }
     curr = nxt;
 }
}
2.2 双向链表

双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱。

2.2.1 非循环双链表

为空时

在这里插入图片描述

非空时

在这里插入图片描述

2.2.2循环双链表
  1. 为空时的情况

    头指针

  2. 非空时

    在这里插入图片描述

  3. 插入

    在这里插入图片描述

题目

写出带头结点的双向循环链表为空表的条件:
L->next == L->prev == L

3. 特殊的线性表----栈

栈 (LIFO) sql: #include

3.1基本知识

特点:后进先出 ,是一种特殊的受限线性表,只能在一端(栈顶)进行插入或删除。

ps:栈是一种数据结构 栈中的元素有逻辑线性关系

  1. 定义:限定仅在一端进行插入或删除的线性表

  2. 数据关系:R1={ <ai-1, ai >| ai-1, ai∈D, i=2, … , n } 约定an 端为栈顶,a1 端为栈底。

  3. 基本操作:

    • 判断是否为空 bool empty
    • 返回元素个数 int length()
    • 弹出栈顶 E pop()
    • 在栈顶压入 bool push(const E &item)
    • 返回栈顶元素 const E& top()
  4. 存储方式:

    • 数组 由于事先声明大小,可能存在空间浪费.所有操作都是常数时间。

    • 链表 每个元素都需要一个链接域,产生结构性开销。采用单链表即可,就在头部进行插入和删除,以免遍历整个链表

      bool push(const E& item)
      {
          node <E> *temp=new node<E>;//失败会自己抛出badmalloc
          temp->next=top;
          temp->elem=item;
          top=temp;
          size++;
          return true;
      }
      E pop()
      {
          if(size==0) return false;//空栈
          E it=top->elem;
          node<E> *temp=top->next;
          delete top;
          top=temp;
          size--;
          return true;    
      }
      
  5. 术语

    • 空栈与满栈

      判断方式:

      1. top=0 //顺序栈为空 元素个数为0
      2. top==size/ /顺序栈为满,元素个数为n
      3. head->next==NULL //空
    • 下溢

      对空栈执行出栈操作

    • 溢出

      对满栈执行入栈

很明显栈太受限了,没有队列那么多考点,但是他的应用还是很重要的!

3.2 栈的应用

3.2.1 进制转化

基本公式:

N = ( N / d ) ∗ d + N   m o d   d N=(N / d) *d + N \bmod d N=(N/d)d+Nmodd
选自yxb老师ppt
所以这就和栈非常相像了。

算法思想

  1. 输入一个十进制整数N
  2. 定义一个栈,甚至可以是char型(0-7)
  3. 只要当前N不为0,重复以下操作:N%8入栈,N=N/8
  4. 若栈非空,执行出栈并将出栈的内容输出

算法 描述

void convert(int N)
{
    stack<char> s;
    while(N!=0)
    {
        s.push(N %8);
        N=n/8
    }
    while(!s.isempty)
    {
       cout<< s.pop();
    }
}

算法分析

时间复杂度: O(logn)

空间复杂度:O(logn) 栈的开销

3.2.2 判别表达式中括号是否配对的算法

梦回编译原理语法分析。蠢蠢的代码,自己写的。。。

算法思想

  1. 输入字符 换行结束
  2. 对于输入的字符,入栈1
  3. 如果输入的是) ,只要栈1不空,没碰到第一个(前,把栈1里的字符压入栈2,同时栈1开始弹出
  4. 如果栈1空了还没有找到(,说明括号不匹配,因此输出错误
  5. 如果找到了,将栈2的内容弹出,输出该括号匹配的字符
  6. 最后输入完成后,检查栈1.如果栈1里还有多余的(,说明括号不匹配,输出错误信息

算法描述

void match()
{
    stack<char> s,s2;
    char ch;while(ch=cin.get())
    {
        s.push(ch);
        if(ch == ')')
        {
            while(!(s.empty()))
            {
                s2.push(s.top());
                s.pop();
                if(s2.top()=='(') break;//找到匹配的左括号了
            }
            if(s2.top()!='(') cout<<"false , lack of '('"<<endl;
            else
            {
                while(!s2.empty())
                {
                    cout<<s2.top();
                    s2.pop();
                }
                cout<<endl;
            }
        }
        if(ch=='\n') break;
    }
   while(!s.empty())
   {
       if(s.top()=='(') {cout<<"false , lack of ')'"<<endl;break;}
       s.pop();
   }
}

常考题目

  1. 判断这种出栈顺序有可能不
  2. 将递归算法转换为对应的非递归算法需要 栈(函数的递归调用本身就在用栈)
3.3.3 乱入— 后缀表达式

我也不知道哪来的,据说是因为后缀表达式的计算是看到数字入栈,看到n元操作符就出栈n个栈内的东西进行运算后再入栈。所以比如这个表达式1 2 3 + * 6 -,1,2,3入栈,看到+出栈2 3 计算后结果为5 入栈 ,看到* 出栈5 1 ,进行运算结果为5入栈,看到6入栈,看到-,出栈6 5 ,进行5-6(注意顺序了!!)。最后-1入栈。

但是一个我们平时见到的表达式,如何表示为 后缀表达式?

例题:

a*(b+c)-d

  • 第一步 按照计算规则加括号:

    ((a*(b+c))-d)

  • 第二步 把运算符移到括号外 (所以刚写的代码还是有用啊!)

    ((a(bc)+)*)d)-

  • 去括号

    abc+*d-

网上也有方法是,先根据中序遍历把这个表达式的二叉树画出来(but 已知中序不能画出唯一的二叉树,如a*(b+c+d)),然后再后序遍历这个二叉树

4. 特殊的线性表—队列

4.1 基本知识

特点:先进先出 时间上越早产生的对象越早被处理。队列中的元素有逻辑线性关系。

  1. 定义

    队列(queue )简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。

  2. 数据关系

    数据关系: R1={ <a i-1,ai > | ai-1, ai∈D, i=2,…,n} 约定其中a1 端为队列头,an 端为队列尾

  3. 基本操作

    virtual void clear() = 0; 
    virtual bool enqueue(const Elem&) = 0;  
    virtual bool dequeue(Elem&) = 0; 
    virtual bool frontValue(Elem&) const = 0; 
    virtual int length() const = 0; 
    
  4. 术语

    插入端:队尾

    删除端:队首

    入队操作:enqueue 出队操作:dequeue

4.2 队列的实现

因为队列的实现相比栈稍微复杂一点,所以专门列了一个模块。注意到我的代码是没有头结点的单链表。

4.2.1 基于链表实现
template <typename E>
bool Lqueue<E>::dequeue(E&it)
{
    if(Size==0) return false;
    it=head->elem;
    node<E> *temp=head;
    head=temp->next;
    delete temp;
    Size--;
    if(Size==0) tail = NULL;//否则tail会变成野指针
    return true;
}
template <typename E>
bool Lqueue<E>::enqueue(const E &it)
{
    node<E> *temp=new node<E>(it,NULL);
    if(Size==0)
    {
        tail=head=temp;
    }
    else
    {
        tail->next=temp;
        tail=temp;
    }
    Size++;
    return true;
}
4.2.2 基于顺序表实现—循环队列

因为头部弹出,尾部增加,如果不设置为循环队列,很有可能最后数组大部分地址是空的也加不进去

循环队列首先需要一个front和rear来存储队首元素和队尾元素的位置编号。注意点:如何判断队列空/满。

空:head==rear -1

rearheadnullnullnullnullnullnull

满情况有很多种:插入-1 0 1 2 3 4 5 6,弹出-1 0插入 7 8

78123456
rearhead
12345678
rearhead

很明显情况一和空的情况重合了,无法判断。解决办法,开n+1的数组,只放n个元素

那么判断空:rear==front-1

判断满: (rear+2)% maxsize==front

4.3 队列的应用
4.3.1 打印杨辉三角
输入:杨辉三角形的含函数 功能:生存杨辉三角形 输出:输出杨辉三角形

梦回ccf夏季小学期。用数组他不香吗。。。

输入n。

首先杨辉三角形第i行第j个元素是i-1行的第j-1和第j个的元素的和。

然后一般在两头 插入 0,方便直接计算。

输入: 8

输出:

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1

算法思想:

  1. aij=ai-1 j-1+ai-1 j

  2. 首先把第一行的1进入队列 然后打印队列,既三角形第一行

  3. 当前待处理行为2

  4. 然后开始处理当前待处理行

  5. 为了方便进行计算,用一个变量s记录ai-1 j-1 ,初始置0。

  6. 为了方便直接从队列中取值运算,在队列中入队0,相当于在上一行最后加一个0.

  7. 每次取出队列中队头的元素,然后将其和变量s相加,结果存入队列。然后更新变量s的值为刚刚取出的队头。执行步骤5直到输出完这一行该输出的数字,既第i行进行i次循环

  8. 打印队列里当前所有元素。

  9. 当前待处理行+1,重复执行2-6,直到处理完第n行

算法代码:

void printRec(int n )
{
    Lqueue<int> a;
    a.enqueue(1);//入队
    a.print();//打印第一行
    int temp=0,s=0;
    for(int i=1; i<n; i++)
    {
        s=0;
        a.enqueue(0);
        for(int j=0; j<=i; j++)
        {
            a.dequeue(temp);//temp存储出队的元素值
            a.enqueue(temp+s);//temp+s入队
            s=temp;//更新s值
        }
        a.print();//打印第i+1行
    }
}

算法分析

  • 时间复杂度:O(n2)
  • 空间复杂度:队列的开销 0(n)
4.3.2 倒置
已知:Q是一个非空队列,S是一个空栈。编写算法,仅用队列和栈的ADT函数和少量工作变量,将队列Q的所有元素逆置。
栈的ADT函数有:

void makeEmpty(SqStack s);  置空栈

void push(SqStack s,ElemType e);  元素e入栈

ElemType pop(SqStack s); 出栈,返回栈顶元素

bool isEmpty(SqStack s);  判断栈空

队列的ADT函数有:

void enQueue(Queue q,ElemType e); 元素e入队 

ElemType deQueue(Queue q);  出队,返回队头元素 

bool isEmpty(Queue q); 判断队空

void convert(Queue a)
{
    stack<ElemType> b;
    ElemType temp;
    while(!a.isEmpty())
    {
        temp=a.dequeue();
        b.push(temp);
    }
    while(!b.isEmpty())
    {
        temp=b.pop();
        a.enqueue(temp);
    }
    b.makeEmpty();
}
  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值