数据结构 - 线性表

428 阅读15分钟

线性表的基本概念

线性表是由n(n >= 0)个数据元素(结点)组成的有限序列.

则n为表的长度, 当n = 0时为空表

非空的线性表(n > 0)的表示方式

L = (a1, a1, …, an)

其中a1起始结点, an终端结点. 对任意一对相邻的结点ai和ai+1 (1 <= i < n), ai称为ai+1直接前驱, ai+1称为ai直接后继.

在这张图片中, 线性表是一个由10个结点组成的有限序列. 表的长度为10, 结点A为起始结点, 结点J为终端结点. 对于任意相邻的两个结点, 例如AB两个结点, 结点A为结点B的直接前驱, 结点B为结点A的直接后继.

特点

  1. 线性表中只有一个起始结点, 一个终端结点
  2. 起始结点没有直接前驱, 有一个直接后继
  3. 终端结点没有直接后继, 有一个直接前驱
  4. 除了起始结点和终端结点, 每个结点都有且只有一个直接前驱和一个直接后继

线性表的顺序存储

将表中的结点依次存放在计算机内存中一组连结的存储单元中. 用顺序存储方式实现的线性表称为顺序表. 一般使用数组来表示顺序表.

已知a1地址为locate(a1), 每个数据占m个存储单元, 则ai的地址

locate(ai) = locate(a1) + m * (i - 1)

顺序存储线性表需要存储

线性表的大小: maxSize

线性表的长度: length

所存放的数据类型: DataType

注意

  1. 顺序表是用一维数组实现的线性表, 数组的下标可以看成是元素的相对地址.
  2. 逻辑上相邻的元素, 存储在物理位置也相邻的单元中

特点

  1. 线性表的逻辑结构与存储结构一致.
  2. 可以对数据元素实现随机读取

基本运算在顺序表上的实现

插入

线性表的插入运算是指在表的第 i (1 <= i <= n+1) 个位置上, 插入一个新结点, 使长度为n的线性表变成长度为n + 1的线性表

前提: 线性表的长度为n, 要插入的位置为i

元素的移动次数: 在i位置插入数据时, 要移动 n - i + 1 个数据元素

平均移动元素个数: 我们可以简单理解为最好的时候, 在末尾插入, 移动0个数据元素, 最坏的时候, 在位置为1的地插入, 移动n个数据元素, 则平均移动元素个数为 n / 2, 则时间复杂度为O(n)

注意事项

1. 当表空间已满, 不可再做插入操作
2. 当插入位置为非法位置, 不可做正常插入操作

代码

/**
 线性表的插入
 
 @param pList 类型为SeqList的结构体指针
 @param x 要插入的类型为DataType类型的数据元素
 @param i 要插入的位置
*/
void insertSeqList(SeqList * pList, DataType x, int i) {
    // 判断线性表是否已满
    if (pList->length == maxSize) {
        printf("表已满 \n");
        return;
    }
    
    // 检查插入位置是否合法
    if (i < 1 || i > (pList->length) + 1) {
        printf("插入位置不合法 \n");
        return;
    }
    
    // 进行插入操作
    // 从表的最后一个位置依次进行后移
    for (int j = pList->length; j >= i; j--) {
        pList->data[j] = pList->data[j-1];
    }
    
    // 后移完成之后, 对空出的位置进行赋值
    pList->data[i - 1] = x;
    
    // 表的长度加1
    pList->length++;
}

删除

线性表的删除运算是指将表的第 i 个结点删去, 使长度为 n 的线性表变成长度为 n-1 的线性表

注意事项

  1. 当要删除元素的位置 i 不在表长范围内 (i < 1 || i > L -> length) 时, 为非法位置, 不能做正常的删除操作

代码

/**
 线性表的删除

 @param pList 类型为SeqList结构体的指针
 @param i 要删除的位置
 */
void deleteSeqList(SeqList * pList, int i) {
    
    // 检查删除位置是否合法
    if (i < 1 || i > pList -> length) {
        printf("删除位置不合法");
        return;
    }
    
    // 进行删除操作, 依次左移
    for (int j = i; j < pList->length; j++) {
        pList->data[j - 1] = pList->data[j];
    }
    
    // 表的长度减1
    pList->length--;
}

元素的移动次数: 在i位置删除数据时, 要移动 n - i 个数据元素

平均移动元素个数: 我们可以简单理解为最好的时候, 在末尾删除, 移动0个数据元素, 最坏的时候, 在位置为1的地删除, 移动 n-1 个数据元素, 则平均移动元素个数为 (n-1) / 2, 则时间复杂度为O(n)

定位 / 查找

定位运算的功能是求L中值等于x的结点序号的最小值, 当不存在这种结点时结果为0

/**
 线性表的定位

 @param pList 类型为SeqList结构体的指针
 @param name 要查找的姓名
 @return 查找到的元素的位置
 */
int locateSeqList(SeqList * pList, char name[]) {
    int i = 0;
    
    // 在顺序表中查找值为name的结点
    while (i < pList->length && pList->data[i].name == name) {
        i++;
    }
    
    // 若找到值为name的结点, 则返回结点的序号
    if (i < pList->length) {
        return i + 1;
    } else { // 未找到值为name的结点, 返回0
        return 0;
    }
}

总结

  1. 插入算法 , 时间复杂度为O(n). 一般情况下, 元素的移动次数为 n-i+1次, 平均移动次数为 n / 2
  2. 删除算法, 时间复杂度为O(n). 元素的平均移动次数为 (n-1) / 2
  3. 定位算法, 比较操作的时间复杂度为O(n). 求表长和读表元素算法的时间复杂度为O(1)

顺序表的优点

  1. 无需为表示结点之间的逻辑关系而增加额外存储空间
  2. 可以方便地随机存取表中的任一结点

顺序表的缺点

  1. 插入和删除运算不方便, 必须移动大量的结点
  2. 顺序表要求占用连结的空间, 当表长变化较大时, 难以确定合适的存储规模

线性表的链接存储

链接方式存储的线性表称为链表. Link List

特点

  1. 用一组任意的存储单元来存放
  2. 链表中结点的逻辑次序和物理次序不一定相同. 还必须存储指示其后继结点的地址信息

结点的结构

{% asset_img node.png %}

数据域, 又为data域, 存放结点中的数据

指针域, 又为next域, 存放结点的直接后继的地址

单链表的类型定义

所有结点通过指针链接而组成单链表.

NULL称为为空指针

Head称为头指针, 存放链表中的第一个结点的地址

单链表中第一个结点内一般不存数据, 称为为头结点

常见的单链表的四种形式

单链表的特点

  1. 起始结点又称为首结点. 没有直接前驱, 故设头指针head指向首结点
  2. 链表由头指针唯一确定, 单链表可以用头指针的名字来命名.
  3. 终端结点又称为尾结点. 没有直接后继, 故终端结点的指针域为空, 即NULL
  4. 除头结点之外的结点为表结点
  5. 为运算操作方便, 头结点中不存数据

单链表的初始化

// 声明一个结构体类型
typedef struct {
    char name[12];
    int age;
} DataType;

// 声明一个单链表类型
typedef struct node {
    DataType data; // 数据域
    struct node * next; // 指针域
} Node, * LinkList;

/**
 初始化一个单链表

 @return 初始化好的单链表
 */
LinkList initiateLinkList() {
    
    // 头指针
    LinkList head;
    
    // 动态创建一个结点, 也就是头结点, 然后让头指针指向这个结点
    head = malloc(sizeof(Node));
    
    // 头结点的指针域为NULL
    head->next = NULL;
    
    // 返回这个单链表
    return head;
}

插入

插入运算是将值为x的新结点 插入到表的第i个结点的位置上, 即插入到ai-1与ai之间

/**
 插入

 @param list 单链表
 @param x 要插入的数据元素
 @param i 要插入的位置
 */
void insertLinkList(LinkList list, DataType x, int i) {
    
    LinkList s, p;
    
    if (i == 1) {
        p = list;
    } else {
        // 找到第 i-1 个数据元素结点
        p = getLinkList(list, i - 1);
    }
    
    if (p == NULL) {
        // 如果不存在, 返回错误信息
        printf("找不到插入的位置");
    } else{
        // 生成新结点并初始化
        s = malloc(sizeof(Node));
        s->data = x;
        
        // 新结点的指针域指向第i个元素
        s->next = p->next;
        
        // 第 i-1 个结点的指针域指向新结点
        p->next = s;
    }
}

删除

在单链表中删除第i个结点的基本操作为: 找到线性表中第i-1个结点, 修改其指向直接后继的指针

/**
 删除

 @param list 单链表
 @param i 要删除结点的位置
 */
void deleteLinkList(LinkList list, int i) {
    LinkList p;
    
    // 找到待删结点的直接前驱
    if (i == 1) {
        p = list;
    } else {
        p = getLinkList(list, i-1);
    }
    
    // 若直接前驱存在, 且待删结点存在
    if (p != NULL && p->next != NULL) {
        
        // q指向待删结点
        LinkList q = p->next;
        
        // 移出待删结点
        p->next = q->next;
        
        // 释放已经移出的结点
        free(q);
    } else {
        printf("要不到要删除的结点");
    }
}

求表长

在单链表存储结构中, 线性表的长度等于单链表所含结点的个数(不含头结点)

/**
 求表的长度

 @param list 单链表
 @return 表的长度
 */
int LengthLinkList(LinkList list) {
    
    // 用于计数
    int j = 0;
    
    // p指向头指针
    LinkList p = list;
    
    // 当下一个结点不为空时, 计数加1, p指向下一个结点
    while (p->next != NULL) {
        j++;
        p = p->next;
    }
    
    // 返回表的长度
    return j;
}

读表元素

/**
 读表元素

 @param list 单链表
 @param i 要读取的位置
 @return 如果有返回读取到的元素, 没有则返回NULL
 */
LinkList getLinkList(LinkList list, int i) {
    
    // 用于计数
    int j = 1;
    
    // p指向首结点
    LinkList p = list->next;
    while (j < i && p != NULL) {
        j++;
        p = p->next;
    }
    
    // 如果j等于i, 则p指向的结点为要找的结点
    if (j == i) {
        return p;
    } else { // 否则没有要找的结点
        return NULL;
    }
}

定位

定位运算是给定表元素的值, 找出这个元素的位置

/**
 定位

 @param list 单链表
 @param name 要查找的结点的名字
 @return 如果查找到了, 则返回结点的位置, 如果没有, 则返回0
 */
int locateLinkList(LinkList list, char name[]) {
    int j = 0;
    
    // p指向首结点
    LinkList p = list->next;
    
    // 查找结点
    while (p != NULL && !(strcmp(p->data.name, name) == 0)) {
        j++;
        p = p->next;
    }
    
    // 如果有则返回序号, 没有返回0
    if (p != NULL) {
        return j + 1;
    } else {
        return 0;
    }
}

其它链表

循环链表

普通链表的终端结点的next值为NULL

循环链表的终端结点的next指向头结点

特点

在循环链表中, 从任一结点出发都能扫描整个链表

双向循环链表

在链表中设置两个指针域, 一个指向后继结点, 一个指向前驱结点, 这就是双向链表

空双向循环链表

非空双向循环链表

双向链表中结点的删除

设p指向待删结点, 删除*p可这样做

p -> prior -> next = p -> next; // p前驱结点的直接后继指向p的后继结点

p -> next -> prior = p -> prior; // p后继结点的直接前驱指向p的前驱结点

free(p); // 释放*p的空间

双向链表中结点的插入

在p所指后面插入一个新的结点t, 插入可这样做

t -> next = p -> next;

t -> prior = p;

p -> next -> prior = t;

p -> next = t;

顺序实现与链接实现的比较

  1. 单链表的每个包括数据域和指针域, 指针域需要占用额外空间
  2. 从整体考虑, 顺序表要预分配存储空间, 如果预先分配得过大, 将造成浪费, 若分配得过小, 又将发生上溢;
  3. 单链表不需要预先分配空间, 只要内存内存空间没有耗尽, 单链表中的结点个数就没有限制.

顺序表的完整代码

#include <stdio.h>

// 定义一个结构体类型
typedef struct {
    char name[10];
    int age;
} DataType;

// 顺序表的结构体定义
// 线性表的大小, 也就是最多存储多少个数据元素
const int maxSize = 100;
typedef struct {
    
    // 所存放的数据类型为int, 最多存放100个元素的数组
    DataType data[maxSize];
    
    // 当前所存放的数据元素的个数
    int length;
} SeqList;

/**
 线性表的插入
 
 @param pList 类型为SeqList的结构体指针
 @param x 要插入的类型为DataType类型的数据元素
 @param i 要插入的位置
*/
void insertSeqList(SeqList * pList, DataType x, int i) {
    // 判断线性表是否已满
    if (pList->length == maxSize) {
        printf("表已满 \n");
        return;
    }
    
    // 检查插入位置是否合法
    if (i < 1 || i > (pList->length) + 1) {
        printf("插入位置不合法 \n");
        return;
    }
    
    // 进行插入操作
    // 从表的最后一个位置依次进行后移
    for (int j = pList->length; j >= i; j--) {
        pList->data[j] = pList->data[j-1];
    }
    
    // 后移完成之后, 对空出的位置进行赋值
    pList->data[i - 1] = x;
    
    // 表的长度加1
    pList->length++;
}

/**
 线性表的删除

 @param pList 类型为SeqList结构体的指针
 @param i 要删除的位置
 */
void deleteSeqList(SeqList * pList, int i) {
    
    // 检查删除位置是否合法
    if (i < 1 || i > pList -> length) {
        printf("删除位置不合法");
        return;
    }
    
    // 进行删除操作, 依次左移
    for (int j = i; j < pList->length; j++) {
        pList->data[j - 1] = pList->data[j];
    }
    
    // 表的长度减1
    pList->length--;
}

/**
 线性表的定位

 @param pList 类型为SeqList结构体的指针
 @param name 要查找的姓名
 @return 查找到的元素的位置
 */
int locateSeqList(SeqList * pList, char name[]) {
    int i = 0;
    
    // 在顺序表中查找值为name的结点
    while (i < pList->length && pList->data[i].name == name) {
        i++;
    }
    
    // 若找到值为name的结点, 则返回结点的序号
    if (i < pList->length) {
        return i + 1;
    } else { // 未找到值为name的结点, 返回0
        return 0;
    }
}

int main(int argc, const char * argv[]) {
    // insert code here...
    
    // 创建一个结构体实例, 并进行初始化
    DataType dt1 = {"alex", 18};
    DataType dt2 = {"kevin", 16};
    DataType dt3 = {"jack", 21};

    // 创建一个空的线性表
    SeqList list = {};
    
    // 创建一个指针指向这个线性表
    SeqList * pList = &list;
    
    printf("插入前表的长度: %d \n", list.length);
    
    // 插入操作
    insertSeqList(pList, dt1, 1);
    insertSeqList(pList, dt2, 1);
    insertSeqList(pList, dt3, 1);
    
    printf("插入后表的长度: %d \n", list.length);
    
    // 删除操作
    deleteSeqList(pList, 2);
    
    printf("插入后表的长度: %d \n", list.length);
    
    // 定位
    int loc = locateSeqList(pList, "alex");
    printf("alex在第%d位 \n", loc);
    
    return 0;
}

单链表的完整代码

#include <stdio.h>
#include <stdlib.h>

// 声明一个结构体类型
typedef struct {
    char name[12];
    int age;
} DataType;


// 声明一个单链表类型
typedef struct node {
    DataType data; // 数据域
    struct node * next; // 指针域
} Node, * LinkList;


/**
 初始化一个单链表

 @return 初始化好的单链表
 */
LinkList initiateLinkList() {
    
    // 头指针
    LinkList head;
    
    // 动态创建一个结点, 也就是头结点, 然后让头指针指向这个结点
    head = malloc(sizeof(Node));
    
    // 头结点的指针域为NULL
    head->next = NULL;
    
    // 返回这个单链表
    return head;
}


/**
 求表的长度

 @param list 单链表
 @return 表的长度
 */
int LengthLinkList(LinkList list) {
    
    // 用于计数
    int j = 0;
    
    // p指向头指针
    LinkList p = list;
    
    // 当下一个结点不为空时, 计数加1, p指向下一个结点
    while (p->next != NULL) {
        j++;
        p = p->next;
    }
    
    // 返回表的长度
    return j;
}


/**
 读表元素

 @param list 单链表
 @param i 要读取的位置
 @return 如果有返回读取到的元素, 没有则返回NULL
 */
LinkList getLinkList(LinkList list, int i) {
    
    // 用于计数
    int j = 1;
    
    // p指向首结点
    LinkList p = list->next;
    while (j < i && p != NULL) {
        j++;
        p = p->next;
    }
    
    // 如果j等于i, 则p指向的结点为要找的结点
    if (j == i) {
        return p;
    } else { // 否则没有要找的结点
        return NULL;
    }
}


/**
 插入

 @param list 单链表
 @param x 要插入的数据元素
 @param i 要插入的位置
 */
void insertLinkList(LinkList list, DataType x, int i) {
    
    LinkList s, p;
    
    if (i == 1) {
        p = list;
    } else {
        // 找到第 i-1 个数据元素结点
        p = getLinkList(list, i - 1);
    }
    
    if (p == NULL) {
        // 如果不存在, 返回错误信息
        printf("找不到插入的位置");
    } else{
        // 生成新结点并初始化
        s = malloc(sizeof(Node));
        s->data = x;
        
        // 新结点的指针域指向第i个元素
        s->next = p->next;
        
        // 第 i-1 个结点的指针域指向新结点
        p->next = s;
    }
}


/**
 删除

 @param list 单链表
 @param i 要删除结点的位置
 */
void deleteLinkList(LinkList list, int i) {
    LinkList p;
    
    // 找到待删结点的直接前驱
    if (i == 1) {
        p = list;
    } else {
        p = getLinkList(list, i-1);
    }
    
    // 若直接前驱存在, 且待删结点存在
    if (p != NULL && p->next != NULL) {
        
        // q指向待删结点
        LinkList q = p->next;
        
        // 移出待删结点
        p->next = q->next;
        
        // 释放已经移出的结点
        free(q);
    } else {
        printf("要不到要删除的结点");
    }
}


/**
 定位

 @param list 单链表
 @param name 要查找的结点的名字
 @return 如果查找到了, 则返回结点的位置, 如果没有, 则返回0
 */
int locateLinkList(LinkList list, char name[]) {
    int j = 0;
    
    // p指向首结点
    LinkList p = list->next;
    
    // 查找结点
    while (p != NULL && !(strcmp(p->data.name, name) == 0)) {
        j++;
        p = p->next;
    }
    
    // 如果有则返回序号, 没有返回0
    if (p != NULL) {
        return j + 1;
    } else {
        return 0;
    }
}

int main(int argc, const char * argv[]) {
    // insert code here...
    
    Node list = {};
    LinkList pList = &list;
    
    DataType dt1 = {"jack", 18};
    DataType dt2 = {"kevin", 20};
    
    // 插入元素
    insertLinkList(pList, dt1, 1);
    insertLinkList(pList, dt2, 1);
    
    // 读表元素
    LinkList p = getLinkList(pList, 2);
    printf("%s \n", p->data.name);
    
    // 删除元素
//    deleteLinkList(pList, 2);
//    deleteLinkList(pList, 1);
    
    // 读表长
    int length = LengthLinkList(pList);
    printf("表的长度为: %d \n", length);
    
    // 定位
    int loc = locateLinkList(pList, "kevin");
    printf("在第%d位 \n", loc);
    return 0;
}