本系列文章为作者复习数据结构过程中的知识点总结、速通,欢迎补充

Written by SJTU-XHW

Reference: 张同珍老师 PPT | UNIkeEN


Chapter 1 数据结构绪论

1.1 数据结构逻辑分类

  • 集合结构:次序任意,重视归属关系;
  • 线性结构:有序序列,有前驱、后继关系;
  • 树状结构:层次关系,分根元素-其他元素;
  • 图状结构:一般的逻辑结构,不限前驱和后继;

1.2 逻辑结构基本运算类型

创、清、增删改查、访问、遍历

1.3 逻辑结构的运算实现

存储结点、数据元素间关系(顺序实现、链接实现、散列实现、索引存储)、附加信息

总结:一个数据结构就是针对某一逻辑结构讨论数据的存储实现运算实现

1.4 算法优劣因素分析

正确性、易读性、健壮性、高效率(时间、空间性能)

  1. 时间复杂度:算法所需运算量 和 问题规模 间的关系

    • 时间复杂度因为会与被处理数据的分布有关,所以会有最好最坏和平均的说法

    • 重要易错点:答题时注意除非说明,必须同时写上:最好、最坏、平均时间复杂度,少一个都不全面

    • 算法运算量计算:不通用性

    • 渐进时间复杂度:通用性

    • 知识补充:多项式时间算法$O(1)\lt O(log\space N)\lt O(N)\lt O(N\space log\space N)\lt O(N^c),\space c\in\mathbf{R}$

      和指数时间算法:$O(2^N)\lt O(N!)\lt O(N^N)$(NP问题可以认为是非多项式时间算法能解决的问题)

    • 渐进时间复杂度计算定理

  1. 空间复杂度:辅助存储空间 与 问题规模 间的关系

    易错点警示:

    1. 链表的指针所占用的空间不算辅助空间:因为这是被处理的数据结构本身的空间;

    2. 递归调用的系统栈算辅助空间:讨论递归算法的空间复杂度一定要注意系统栈的情况!

Chapter 2 线性表

2.1 零碎概念集合

  • 线性结构的定义:n(n ≥ 0)个结点的有穷序列 $(a_0, a_1,…,a_{n-1})$

    起始结点、终端结点、元素的序号/位置、直接前驱、直接后继、空表的定义

  • 线性表的地位:处理线性结构的工具;

  • 线性表的基本运算种类:创清、增删查、访问遍历、求长;

  • 均摊分析������

2.2 顺序实现的线性表

  1. 存储实现:数组是天然的线性结构(连续空间)【数组容量、数组指针、数组长度】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template <class T>
    class seqList {
    private:
    T* data;
    int maxSize;
    int length;

    void doubleSpace();

    public:
    seqList(int capacity=10);
    ~seqList() { if (data) delete[] data; }
    bool isempty() const;
    int length() const;
    void clear();
    void insert(int i, const T& dt);
    void remove(int i, T& x);
    T visit() const;
    void traverse() const;
    };
  2. 运算实现

    • 创建运算:开辟空间、设定表长和规模

      1
      2
      3
      4
      template <class T>
      seqList<T>::seqList(int capacity): length(0), maxSize(capacity) {
      data = new T[capacity];
      }
    • 清空运算$\textbf{O(1)}$:直接将表长置0

      1
      2
      template <class T>
      void seqList<T>::clear() { length = 0; }
    • 判空运算$\textbf{O(1)}$:直接判断表长不为0

      1
      2
      template <class T>
      bool seqList<T>::isempty() const { return length == 0; }
    • 插入运算$\textbf{O(1)}-\textbf{O(n)}-\textbf{O(n)}$:先看空间是否够,不够扩容,够依此向后移位;记得维护长度!!!

      1
      2
      3
      4
      5
      6
      7
      8
      template <class T>
      void seqList<T>::insert(int i, const T& dt) {
      if (length == maxSize) doubleSpace();
      for (int idx = length - 1; idx >= i; --idx) {
      data[idx + 1] = data[idx];
      }
      data[i] = dt; ++length;
      }

      扩容运算$\textbf{O(n)}$

      1
      2
      3
      4
      5
      6
      7
      8
      9
      template <class T>
      void seqList<T>::doubleSpace() {
      T tmp = data;
      data = new T[maxSize * 2];
      for (int i = 0; i < length; ++i)
      data[i] = tmp[i];
      delete[] tmp;
      maxSize *= 2;
      }

      重点:均摊法分析:每过n、2n、…的时机,可能会进行 O(n) 的扩容。均摊下来,平均时间复杂度还是相当于单次插入的 O(n)。

  • 删除运算$\textbf{O(1)}-\textbf{O(n)}-\textbf{O(n)}$:先看是否为空,不空则后面元素向前覆盖,记得维护表长!!!

    1
    2
    3
    4
    5
    6
    7
    8
    template <class T>
    void seqList<T>::remove(int i, T& x) {
    if (length == 0) throw noElementException();
    x = data[i];
    for (int idx = i + 1; idx < length; ++idx)
    data[i - 1] = data[i];
    --length;
    }
  • 访问和遍历$\textbf{O(n)}$:无需多言

    1
    2
    3
    4
    5
    6
    7
    8
    template <class T>
    T seqList<T>::visit(int i) const { return data[i]; }

    template <class T>
    void seqList<T>::traverse() const {
    for (int i = 0; i < length; ++i)
    std::cout << data[i] << ' ';
    }
  • 查找运算$\textbf{O(n)}$:”哨兵技巧“

    1
    2
    3
    4
    5
    6
    template <class T>
    int seqList<T>::find(const T& dt) const {
    for (int i = length - 1; i >= 0; --i)
    if (data[i] == dt) return i;
    return -1;
    }
  1. 易错点

    • 表长度值忘记维护;
    • 混淆 length 和 maxSize 的意义;
    • 检查异常处理;

2.3 链接实现的线性表

  • 让每个结点保存相关的结点地址+本身数据,来保存结点之间的关系;
  • 链接实现不仅可以用来表示线性结构,还能表示许多非线性结构,例如集合、树、图等;
  • 避免了顺序表插入、删除的元素大规模移动;
  • 动态存储;
  1. 单链表

    • 存储实现:结点 = 数据+后继指针×1;【单链表头、单链表长(不必要)】

      添加”头结点“以一般化一些特殊情况!

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      template <class T>
      class SLinkedList { //0-index.
      private:
      struct sNode {
      T data;
      sNode* next;
      sNode() { next = 0; }
      sNode(T dt, sNode* n=0)
      : data(dt), next(n) {}
      };

      sNode* head;
      int length;
      // (注:此法无需存在,用前后指针法也行)
      sNode* move(int i) const;
      public:
      SLinkedList();
      void clear();
      int length() const;
      bool isempty() const;
      void insert(int i, const T& dt);
      void remove(int i, T& x);
      T visit(int i) const;
      void travese() const;
      ~SLinkedList() { if (head) clear(); }
      };
    • 运算实现(含有哑结点)

      创建运算:赋头指针、置表长;

      1
      2
      3
      4
      template <class T>
      SLinkedList<T>::SLinkedList() {
      head = new sNode(); length = 0;
      }

      清空运算$\textbf{O(n)}$:表长置0,循环释放

      1
      2
      3
      4
      5
      6
      7
      8
      9
      template <class T>
      void SLinkedList<T>::clear() {
      length = 0;
      while (head) {
      sNode* tmp = head;
      head = head->next;
      delete tmp;
      }
      }

      求长、判空$\textbf{O(1)}$:略

      (PRIVATE) 移动指针$\textbf{O(1)}-\textbf{O(n)}-\textbf{O(n)}$(注:此法无需存在,用前后指针法也行)

      1
      2
      3
      4
      5
      6
      7
      template <class T>
      sNode* SLinkedList<T>::move(int i) const {
      sNode* ans = head;
      for (int idx = 0; idx <= i && idx < length; ++idx)
      ans = ans->next;
      return ans;
      }

      插入运算(复杂度取决于移动指针):移动指针至指定位置的直接前驱,插入,记得维护表长

      1
      2
      3
      4
      5
      6
      template <class T>
      void SLinkedList<T>::insert(int i, const T& dt) {
      if (i < 0 || i >= length) return;
      sNode* cur = move(i - 1);
      cur->next = new sNode(dt, cur->next); ++length;
      }

      删除运算(复杂度取决于移动指针):移动指针至指定位置的直接前驱,删除,记得维护表长

      1
      2
      3
      4
      5
      6
      7
      8
      template <class T>
      void SLinkedList<T>::remove(int i, T& x) {
      if (i < 0 || i >= length) return;
      sNode* cur = move(i - 1);
      sNode* tmp = cur->next;
      cur->next = tmp->next;
      delete tmp; --length;
      }

      访问运算(……):移动……,返回;

      1
      2
      3
      4
      5
      6
      template <class T>
      T SLinkedList<T>::visit(int i) const {
      if (i < 0 || i >= length) return;
      sNode* cur = move(i);
      return cur->data;
      }

      查找运算$\textbf{O(n)}$:同顺序表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      template <class T>
      int SLinkedList<T>::find(const T& dt) const {
      sNode* cur = head->next;
      for (int i = 0; cur; ++i) {
      if (cur->data == dt) return i;
      cur = cur->next;
      }
      return -1;
      }

      遍历运算$\textbf{O(n)}$:同~

      1
      2
      3
      4
      5
      6
      7
      8
      template <class T>
      void SLinkedList<T>::traverse() const {
      sNode* cur = head->next;
      while (cur) {
      std::cout << cur->data << ' ';
      cur = cur->next;
      }
      }
    • 易错点

      1. 循环条件设置不好,导致空指针的访问;
      2. 指针操作有误,导致内存泄漏(尤其是删除结点的时候);
      3. 操作野指针;
  • 经典考试题(以至于不放在题型板块):单链表的逆序,要求线性时间、常数空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <class T>
    void SLinkedList<T>::reverse() {
    sNode* tail = head->next, pre;
    if (!tail) return;
    while (pre = tail->next) {
    tail->next = pre->next;
    pre->next = tail;
    head->next = pre;
    }
    }
  1. 单循环链表:循环链表可以没有头结点,但一定要有头指针(指向首个结点,空表nullptr)

    经典考题:约瑟夫环问题

  2. 双向链表、双循环链表:了解前面的思路即可知道如何完成,重要程度较轻;

    双向链表引入头结点、尾结点两个哑结点;

2.4 顺序表和链接表 的 对比与总结

顺序实现 链接实现
空间效率高 插入删除效率高
定位访问灵活 灵活动态存储
实现简单 整体调度较繁琐
可随机访问 仅支持顺序访问

EX - STL 库中的线性结构

  • 容器、迭代器的概念
  • list:类似链接实现,仅能通过迭代器访问;
  • vector:类似顺序实现,可以通过迭代器,也可通过下标访问;
  • deque:优化的线性容器,两端插入删除像list一样快,下标访问像vector一样方便;