C++ 数据结构第二章 ----- 线性表


线性表

线性表的顺序存储


一、基本概念
  1. 定义:线性表的顺序存储指的是 用一组地址连续的存储单元 依次存储线性表中的数据元素。
  2. 注意:
    • 在顺序存储中,逻辑上相邻的数据元素,物理上也相邻。
    • 顺序表是 随机存取 的存储结构:顺序表中数据元素的存储地址是其序号的线性函数,只要确定了存储顺序表的起始地址,计算任意一个元素的存储地址的时间是相等的。
二、基本操作
const int MaxSize = 100;
template <typename DataType>
class SeqList
{
public:
    SeqList();
    SeqList(DataType a[], int n);
    ~SeqList();

    // 将元素x插入第i个位置
    void Insert(int i, DataType x);
    // 删除第i个位置的元素
    DataType Delete(int i);

private:
    // 线性表底层数组
    DataType data[MaxSize];
    // 线性表长度
    int length;
};
  1. 插入操作

    template <typename DataType>
    void SeqList<DataType>::Insert(int i, DataType x) {
    
        // 判断 i 是否合法
        if (i <= 0 || i > length + 1) {
            cout << "输入的位置序号不合法" endl;
            return;
        }
        
    
        // 判断一下插入新元素后顺序表是否溢出
        if (length + 1 > MaxSize) {
            cout << "上溢" << endl;
            return;
        }
    
        // 位移数组元素:最后一个空位置等于倒数第一个顺序表中元素,倒数第二个等于倒数第三个 ...
        for (int j = length; j > i - 1; j--) {
            data[j] = data[j - 1];
        }
    
        // 修改第 i 个位置的数据
        data[i - 1] = x; 
    
        // 长度 + 1
        length++;
    }
    

    算法思想:

    1. 判断插入的位置 i 是否合法
    2. 判断顺序表的存储空间是否已满
    3. 将第 n 之第 i 个位置的元素依次往后移动一个位置,空出第 i 个位置的空间
    4. 将新元素插入到第 i 个位置
    5. 顺序表长度 + 1

    算法时间复杂度分析:

    1. 最好情况:在表尾部插入数据 (即 i = len + 1),此时为 O(1)
    2. 最坏情况:在表头插入 (即 i = 1),此时为 O(n)

    注意:

    1. 当插入的位置为第 i 个结点的时候,需要移动 len - i + 1 个元素,len 为插入数据之前数组的长度
    2. 可以在顺序表的表尾插入元素,无法在表尾之后的位置插入元素了,因为表尾部之后都是空数据
    3. 插入数据前的顺序表的 len 可以理解为顺序表最后一个元素的后一个空位置
    4. 第 i 处的元素对应于数组中下标为 i - 1 的元素
  2. 删除操作

    template <typename DataType>
    DataType SeqList<DataType>::Delete(int i) {   
    
        DataType x;
        // 判断一下删除的位置是否合法
        if (i <= 0 || i >= length) {
            cout << "你输入的位置不合法" << endl;
            return x;
        }
    
        // 拷贝一下数据
        x = data[i - 1];
    
        // 从第 i 到 len - 2 的每一个位置的后一个元素覆盖前面的元素
        for (int n = i - 1; n < length - 1; n++) {
            data[n] = data[n + 1];
        }
    
        // 长度 - 1
        length--;
    
        // 返回删除的元素
        return x;
    }
    

    算法思想:

    1. 判断输入的位置 i 是否合法
    2. 将欲删除的元素保留在 x 中
    3. 将第 i + 1 至 n 个位置的元素依次向前移动一个位置
    4. 长度 - 1
    5. 返回 x

    算法时间复杂度分析:

    1. 最好情况:在顺序表尾部删除元素即 (i = len),此时为 O(1)
    2. 最坏情况:在表头删除元素即 (i = 1),此时为 O(n)

    注意:

    1. 当删除的位置为第 i 个结点时,需要移动 len - i 个元素

线性表的链式存储


这里来说明一下头结点、首元结点、头指针的概念:

  1. 头结点:不存放实际的数据,只是为了操作方便而定义的。
  2. 头指针:指向链表中头结点的指针。
  3. 首元结点:链表中存储第一个元素数据的结点,其在头结点的后面。
一、基本概念
  1. 定义:线性表的链式存储指的是 通过一组任意的存储单元(不要求连续) 来存储线性表中的数据,对每个元素除了存储自身信息外,还需要 存放一个指向其后继的指针
  2. 注意:
    • 线性表的链式存储又称为单链表。
    • 逻辑上相邻的元素数据,物理上未必相邻。
    • 每个元素和其后继指针统称为单链表结点。
    • 单链表结点中有 data 域用于存储元素数据,next 域用于存储下一个结点的内存地址。
    • 单链表是一个非随机存取的存储结构:如果想从单链表中取出某个元素,那么必须从链表头部一点点往后面找。
二、基本操作
template <class T>
struct Node
{
    // 存放元素数据
    T data;
    // 存放下一个结点的内存地址
    Node<T> *next;
};

template <class T>
class LinkList
{
public:
    LinkList();              //建立只有头结点的空链表
    LinkList(T a[], int n, int direction);  //建立有n个元素的单链表

    int Length();            //求单链表的长度
    T Get(int i);            //取单链表中第i个结点的元素值
    int Locate(T x);         //求单链表中值为x的元素序号
    void Insert(int i, T x); //在单链表中第i个位置插入元素值为x的结点
    T Delete(int i);         //在单链表中删除第i个结点
private:
    Node<T> *first; //单链表的头指针
};
  1. 链表的建立

    template <class T>
    LinkList<T>::LinkList() {
        // 创建一个头结点,用单链表的头指针指向头结点
        first = new Node<T>;
    
        // 令头结点的后继为 NULL
        first->next = NULL;
    }
    
    template <class T>
    LinkList<T>::LinkList(T a[], int n, int direction) {
        // 使用尾插法构造链表
        if (direction == 1) {
            // 创建一个头结点,用单链表的头指针指向头结点
            first = new Node<T>;
    
            // 扫描指针指向头结点
            Node<T> *temp = first;
    
            // 遍历数组 a
            for (int i = 0; i < n; i++) {
    
                // 为 a[i] 创建新结点
                Node<T> *newNode = new Node<T>;
                newNode->data = a[i];
    
                // 扫描指针当前指向的结点的后继指向新结点
                temp->next = newNode;
    
                // 扫描指针指向新结点
                temp = temp->next;
            }
    
            // 为最后一个新结点的后继赋值为 null
            temp->next = NULL;            
        } else {
            
            // 使用尾插法构造链表
    
            // 创建一个头结点,用链表的头指针指向头结点
            first = new Node<T>;
    
            for (int i = 0; i < n; i++) {
    
                // 为 a[i] 创建一个新结点
                Node<T> *newNode = new Node<T>;
                newNode->data = a[i];
    
                // 新结点的后继指向头结点后继
                newNode->next = first->next;
    
                // 头结点的后继指向新结点
                first->next = newNode;
            }
        }
    }
    

    算法思想:
    (1) 头插法

    1. 用链表的头指针指向新创建的头结点
    2. 用一个扫描指针指向头结点
    3. 遍历数组
    4. 在遍历过程中,为每一个元素数据创建一个新的结点
    5. 扫描指针的后继指向新的结点
    6. 扫描指针指向新的结点
    7. 遍历完成后,扫描指针指向了最后一个新的结点,令其后继为 NULL

    (2) 尾插法

    1. 用链表的头指针指向新创建的头结点
    2. 遍历数组
    3. 在遍历过程中,为每一个元素数据创建一个新的结点
    4. 让新结点的后继指向头指针的后继
    5. 让头结点的后继指向新结点

    注意:

    1. 尾插法建立单链表的结点顺序和传入的数组的顺序一致,因为尾插法相当于往链表尾部 append 一个个数据
    2. 头插法建立单链表的结点顺序和传入的数组的顺序相反,因为每一次从数组中取出来的数据都放在了头结点后面的一个新插入的结点中,那么数组中最后一个元素一定是在首元结点中的
  2. 求单链表长度

    template<class T>
    int LinkList<T>::Length() {
        int cnt = 0;
    
        // 扫描指针指向头结点
        Node<T> *temp = first;
        while (temp->next != NULL) {
            cnt++;
            temp = temp->next;
        }
        return cnt;
    }
    

    算法思想:

    1. 定义一个变量 cnt 用来计数结点个数
    2. 用一个扫描指针指向头结点
    3. 使用一个 while 循环,只要扫描指针所指向的结点的后继不为空则进行循环
    4. while 循环内部让 cnt 自增 1 ,使扫描指针指向其后继结点
    5. 循环结束后返回 cnt

    注意:

    1. 该思想可以用作遍历单链表的思想
  3. 按序号查找结点值

    template<class T>
    T LinkList<T>::Get(int i) {
        int order = 0;
        Node<T> *temp = first;
        while (temp->next != NULL) {
            order++;
            temp = temp->next;
            if (order == i) {
                return temp->data;
            }
        }
        throw "未查找到";
    }
    
  4. 按结点值查找序号

    template<class T>
    int LinkList<T>::Locate(T x) {
        int order = 0;
        Node<T> *temp = first;
        while (temp->next != NULL) {
            order++;
            temp = temp->next;
            if (temp->data == x) {
                return order;
            }
        }
        throw "未查找到";
    }
    
  5. 插入新结点操作

    template <class T>
    void LinkList<T>::Insert(int i, T x) {
    
        // 记录 temp 指针的后一个位置
        int backOrder = 0;
    
        // 扫描指针指向头结点用于遍历链表
        Node<T> *temp = first;
    
        while (temp->next != NULL) {
    
            // 扫描指针后移
            backOrder++;
    
            // 如果当前位置与输入的位置相同则进行数据插入
            if (backOrder == i) {
    
                // 为 x 数据创建一个结点
                Node<T> *newNode = new Node<T>;
                newNode->data = x;
    
                // 新结点的后继指向扫描指针的后继
                newNode->next = temp->next;
    
                // 扫描指针指向的当前结点(新结点的前一个结点)的后继指向新结点
                temp->next = newNode;
            }
            temp = temp->next;
        }
    
    }
    

    算法思想:

    1. 定义一个变量 backOrder 记录扫描指针所在的后一个位置
    2. 定义一个扫描指针指向头结点
    3. 如果扫描指针的后继不是空则进入 while 循环,backOrder 自增 1
    4. 如果 backOrder 等于了输入的位置 i ,则为 x 创建一个新结点
    5. 令新结点的后继指向扫描指针的后继,扫描指针的后继指向新结点
    6. 如果 backOrder 没有匹配上 i ,则继续往链表后面扫描

    注意:

    1. 这里的 backOrder 记录的是扫描指针的后一个位置
  6. 删除结点操作

    template<class T>
    T LinkList<T>::Delete(int i) {
    
        // 记录扫描指针当前所在位置
        int order = 0;
    
        // 扫描指针
        Node<T> *temp = first->next;
    
        // 前扫描指针
        Node<T> *preTemp = first;
    
        while (temp != NULL) {
            order++;    
    
            if (order == i) {
                // 拷贝一下当前扫描指针指向的结点
                Node<T> *oldNode = temp;
    
                // 拷贝数据
                T x = oldNode->data;
    
                // 扫描前指针指向的结点的后继指向扫描指针的后继
                preTemp->next = temp->next;
    
                // 释放内存
                delete oldNode;
    
                return x;
            }
            preTemp = temp;
            temp = temp->next;
        }
        throw "输入的序号不合法";
    }
    

    算法思想:

    1. 创建一个 order 变量用来记录扫描指针当前所在位置
    2. 扫描指针指向头指针的后继
    3. 创建一个前扫描指针,其始终在扫描指针的前一个位置
    4. 扫描指针扫描单链表,如果其所在位置匹配了 i,则令扫描前指针的后继等于扫描指针的后继,拷贝扫描指针当前结点和结点数据,释放结点内存,返回结点存放的元素数据
    5. 如果没有匹配,则扫描指针和前扫描指针继续往后扫描

    注意:

    1. 前扫描指针 preTemp 始终在扫描指针的前面一个位置
三、双链表

双链表是单链表的改进,其结点新增加了一个 prior 域,保存其前驱内地地址。

template <class T>
struct DNode
{
    // 存放元素数据
    T data;
    // 存放上一个结点的内存地址
    DNode<T> *prior;
    // 存放下一个结点的内存地址
    DNode<T> *next;
};

template <class T>
class DLinkList
{
public:
    DLinkList();              //建立只有头结点的空链表
    DLinkList(T a[], int n, int direction);  //建立有n个元素的单链表
    ~DLinkList();             //析构函数

    void Insert(int i, T x); //在单链表中第i个位置插入元素值为x的结点
    T Delete(int i);         //在单链表中删除第i个结点
private:
    DNode<T> *first; //单链表的头指针
};

注意:

  1. 双链表中 按值查找按位查找 的操作和单链表相同。
  2. 双链表中 插入删除 操作与单链表有很大的不同。
(1) 双链表的插入操作
template <class T>
void DLinkList<T>::Insert(int i, T x)
{
    int order = 0;

    DNode<T> *temp = first;

    while (temp->next != NULL)
    {
        order++;
        temp = temp->next;

        if (order == i)
        {
            // 创建一个新结点
            DNode<T> *newNode = new DNode<T>;
            newNode->data = x;

            // 扫描指针的前一个结点的后继指向新结点
            temp->prior->next = newNode;
            // 新结点的前驱指向扫描指针前一个结点
            newNode->prior = temp->prior;
            // 扫描指针的前驱指向新结点
            temp->prior = newNode;
            // 新结点的后继指向扫描指针
            newNode->next = temp;
        }
    }
}

算法思想:

  1. 当 order 与 i 匹配时,扫描指针 temp 处于插入位置两个结点中的后一个结点
  2. 先让前一个结点的后继指向新结点,然后让新结点的前驱指向前一个结点
  3. 再让扫描指针所在结点的前驱指向新结点,然后让新结点的后继指向扫描指针

注意:

  1. 如果扫描指针处于插入位置两个结点的前一个结点
    • 先让后一个结点的前驱指向新结点,然后让新结点的后继指向后一个结点
    • 再让扫描指针所在结点的后继指向新结点,然后让新结点的前驱指向扫描指针所在结点
(2) 双链表的删除操作
template <class T>
T DLinkList<T>::Delete(int i) {
    int order = 0;
    DNode<T> *temp = first;

    while (temp->next != NULL) {
        order++;
        temp = temp->next;

        if (order == i) {
            // 扫描指针前一个结点的后继指向扫描指针的后一个结点
            temp->prior->next = temp->next;

            // 扫描指针后一个结点的前驱指向扫描指针前一个结点
            temp->next->prior = temp->prior;
        }
    }
}
四、循环链表
  1. 定义:单链表的最后一个结点的 next 域指向头结点的链表
  2. 注意:
    • 循环单链表中没有 next 域为 NULL 的结点,所以判空的方法和单链表不同,一个空的循环单链表,其头结点的 next 域指向的是其本身,只需要判断 temp->next == temp 即可
    • 循环单链表的插入和删除操作基本上和单链表的操作一致,需要注意的就是表尾处的最后一个结点需要指向其表头
五、循环双链表
  1. 定义:循环双链表中,将其最后一个结点的 next 域指向了头结点,头结点的 prior 域指向了其最后一个结点
  2. 注意:
    • 循环双链表中也没有为空的 next 域和 prior 域,所以只需要判断 temp->next == temp 即可
    • 循环双链表的插入和删除操作基本上和双链表的操作一致,需要注意的就是要保存循环的特性

顺序表与链表的比较


顺序表链表
存取方式顺序存取、随机存取只能顺序存取
存储结构逻辑上相邻,物理上也相邻逻辑上相邻、物理上不一定相邻
插入、删除操作需要移动大量元素只需要修改相关结点的指针
空间分配静态分配时需要预分配足够大的空间,动态分配时可能会移动大量元素在需要时申请即可,并且可以扩充
存储密度存储密度大存储密度小
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值