软件设计师 - 第3章 数据结构

概述

按照存储结构来分,数据结构可以分成如下四种:

  • 线性结构:数据元素间呈现线性关系,有单一的前驱和后继
  • 表:可以看做是线性结构的推广,是多个线性结构的集合
  • 树:不同于线性结构,其元素可以有多个直接后继
  • 图:不同于线性结构和树,其元素可以有多个直接前驱和后继

线性结构

线性表

线性表是n个元素的有序序列,非空线性表有如下特点:

  • 存在唯一的第一个元素
  • 存在唯一的最后一个元素
  • 除第一个元素外其余元素都有且只有一个前驱
  • 除最后一个元素外其余元素都有且只有一个后继

线性表的存储方式有两种:顺序存储、链式存储。

顺序存储

采用顺序存储时,就是平常使用的一维数组,可通过数组的索引直接访问对应元素,但进行插入和删除操作时会麻烦些。

cpp 复制代码
// 声明一个固定长度为8,采用顺序存储的线性表 ------ 数组
int array[8];
for (int i = 0; i < 8; i++) {
    array[i] = i;
}

访问操作

对于采用顺序存储的线性表,若要访问位置n处的数据,只需使用下表即可,效率高。

cpp 复制代码
// 访问位置5处的数据
int result = array[5];

插入操作

对于采用顺序存储的固定长度的线性表,若所有位置均被使用,此时若要在位置n处插入新的数据需要重新申请一块空间,将原空间中前n-1个元素复制到新空间,在n处填入新数据,将原空间中n处及其后的数据复制到n+1处及其后;若原空间有剩余位置未被使用,只需将n处及其后的数据"后移",在n处填入新数据即可。

可见对于顺序存储的结构进行插入操作时需要进行大量的"移位"操作,效率低。

cpp 复制代码
// 在位置5处插入数据6
int array2[9];
for (int i = 0; i < 5; i++) {
    array2[i] = array[i];
}
array2[5] = 6;
for (int i = 6; i < 9; i++) {
    array2[i] = array[i - 1];
}

删除操作

对于采用顺序存储的线性表,若要删除位置n处的数据,需要将n+1及其后的数据"前移"。

可见对于顺序存储的结构进行删除操作时需要进行大量的"移位"操作,效率低。

cpp 复制代码
// 删除位置5处数据
for (int i = 5; i < 8 - 1; i++) {
    array[i] = array[i + 1];
}
array[7] = -1;

链式存储

采用链式存储是,除了需要存储数据数据外还需要存后继/前驱节点指针,当节点信息中只存一种(前驱或后继)指针时称为单向链表 ,两者都存时称为双向链表 。单向链表中最后节点的后继指向第一个节点时形成的链表称为循环链表 ,同理,单向链表中第一个节点的前驱指向最后节点时形成的链表称为循环链表 ,双向链表中第一个节点的前驱指向最后节点、最后节点的后继指向第一个节点时形成的链表称为循环链表 。还有一种特殊的链表,其利用顺序储存方式存储指向数据的指针,该结构称为静态链表。采用链式存储是访问特定位置元素需要从头开始遍历,效率低,但删除/删除操作效率比顺序存储高。

cpp 复制代码
// 定义链式存储中节点结构
typedef struct node {
    int data;
    struct node *next;
} NODE, *LinkList;

// 声明一个采用链式存储的线性表
LinkList head = NULL, tail = NULL;
for (int i = 0; i < 8; i++) {
    NODE* newNode = (NODE*)malloc(sizeof(NODE));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = i;
    newNode->next = NULL;
    if (head == NULL) {
        head = newNode;
    } else {
        tail->next = newNode;
    }
    tail = newNode;    
}

访问操作

对于采用链式存储的线性表,若要访问位置n处的数据,需要从头开始遍历,直到遍历到位置n为止,效率低。

cpp 复制代码
NODE* getNode(int index) {
    NODE *node = head;
    for (int i = 0; i < 5; i++) {
        if (node != NULL) {
            node = node->next;
        } else {
            return node;
        }
    }
    return node;
}

// 访问位置5处的数据
NODE* node = getNode(5);
if (node == NULL) {
    printf("Linked List Index Out of Bounds\n");
    exit(1);
}
int result = node->data;

插入操作

对于采用链式存储的线性表,若要在位置n处插入新数据只需要修改n-1处的指针即可。

cpp 复制代码
// 在位置5处插入数据6
NODE* node = getNode(5 - 1);
if (node == NULL) {
    printf("Linked List Index Out of Bounds\n");
    exit(1);
}
NODE* newNode = (NODE*)malloc(sizeof(NODE));
if (newNode == NULL) {
    printf("Memory allocation failed\n");
    exit(1);
}
newNode->data = 6;
newNode->next = node->next;
node->next = newNode;

删除操作

对于采用链式存储的线性表,若要删除位置n处数据只需要修改n-1处的指针即可。

cpp 复制代码
// 删除位置5处数据
NODE* node = getNode(5 - 1);
if (node == NULL) {
    printf("Linked List Index Out of Bounds\n");
    exit(1);
}
if (node->next != null) {
    NODE* temp = node->next;
    node->next = temp->next;
    free(temp);
}

栈是一种线性结构,只能访问一端,修改栈内数据时只能是后进先出(Last In First Out, LIFO),栈的一端称为栈顶,另一端称为栈底,没有数据时称为空栈。一般支持如下运算:

  • 初始化:创建一个空栈
  • 判空:判断栈是否为空栈
  • 入栈:将数据放入栈顶,更新栈顶指针
  • 出栈:将栈顶数据删除,更新栈顶指针
  • 读栈顶数据:只读栈顶数据,不删除,不更新栈顶指针

栈的应用场景有很多,常见的如下:

  • 表达式求值
  • 括号匹配

队列

队列是一种线性结构,是一种先进先出(First In First Out, FIFO)的结构,只允许在一端插入数据,该端称为对尾,在另一端删除数据,该端称为对头。一般支持如下运算:

  • 初始化:创建一个空队列
  • 判空:判断队列是否为空栈
  • 入队:将数据放入队尾,更新队尾指针
  • 出对:将对头数据删除,更新对头指针
  • 读对头数据:只读对头数据,不删除,不更新对头指针

队列的应用场景有很多,常见的如下:

  • 任务队列
  • 消息队列

当队列采用顺序存储时会有些问题,下面举例说明

cpp 复制代码
// 使用数组模拟队里,此时队列为空
int array[8];
int *front = array;
int *rear = array;
// 连续队列5个数据
for (int i = 0; i < 5; i++) {
    *rear = i;
    rear = rear + 1;
}
// 连续出队4个数据
for (int i = 0; i < 4; i++) {
    *front = -1;
    front = front + 1;
}
// 此时队列中只有一个元素,但此时队尾只能添加3个元素

由于上述问题的存在,所有在使用顺序结构作为队列时使用了循环队列的概念,即队尾逻辑上下一个元素是队头,此时便可利用出队的空间。

cpp 复制代码
#define SIZE 8

int array[SIZE];
int *front = array;
int *rear = array;
// 记录队列中的元素数量
int count = 0;

// 判断队列是否满,只有在限制队列长度时才需要判断,比如本例中队列长度为8
bool isFull() {
    return count == SIZE;
}

// 判断队列是否为空,此时front与rear指向同一个位置
bool isEmpty() {
    return count == 0;
}

// 入队
void enqueue(int value) {
    if (isFull()) {
        printf("Queue is full\n");
        return;
    }
    *rear = value;
    // 如此时rear已经是顺序结构的最后一个元素,则指向第一个元素
    rear = (rear == array + SIZE - 1) ? array : rear + 1;
    count++;
}

// 出队
void dequeue() {
    if (isEmpty()) {
        printf("Queue is empty\n");
        return;
    }
    // 如此时front已经是顺序结构的最后一个元素,则指向第一个元素
    front = (front == array + SIZE - 1) ? array : front + 1;
    count--;
}

是一种特殊的线性表,数据元素是字符,有几个概念:空串,空格串,子串,串相等,串比较等,有如下几个基本操作:

  • 赋值操作
  • 连接操作
  • 求长度操作
  • 串比较操作
  • 求子串操作

子串的定位操作通常称为串的模式匹配,子串也称模式串。

朴素的模式匹配算法

该算法也称布鲁特-福斯算法,基本思想是从主串的第一个字符开始匹配子串,若相同则匹配下一个字符,完全匹配成功则返回,否则从主串的第二个字符还是匹配,......。

主串长度n,匹配串长度m。最好情况匹配成功实践复杂度O(n+m),最坏情况的时间复杂度O(n*m)。

改进的模式匹配算法

改进的模式匹配算法又称KMP算法。待补充

相关推荐
苓诣1 小时前
反转链表
数据结构·链表
南宫生1 小时前
力扣-Hot100-二叉树其一【算法学习day.32】
数据结构·学习·算法·leetcode
鸣弦artha2 小时前
蓝桥杯——数组
java·数据结构·算法·蓝桥杯·eclipse·排序算法
析木不会编程3 小时前
【数据结构】【线性表】循环链表(附C语言源码)
c语言·数据结构·链表
孤寂码农_defector3 小时前
C++清除所有输出【DEV-C++】所有编辑器通用 | 算法基础NO.1
数据结构·c++·算法·编辑器
菜喵一只4 小时前
计算2的N次方
数据结构·算法
OKkankan5 小时前
单链表算法题(数据结构)
c语言·数据结构·数据库·c++·算法
yangmc048 小时前
判断子序列
开发语言·数据结构·c++·算法·矩阵·图论
席万里8 小时前
C++图案例大全
数据结构·c++·算法