从零开始的数据结构与算法(三):线性表

337 阅读6分钟

1 定义

线性结构是一个有序数据元素集合的逻辑结构,数据元素之间存在着一对一的线性关系。

  • 集合中必存在唯一的一个“第一个元素”;
  • 集合中必存在唯一的一个“最后的元素”;
  • 除最后的元素之外,其他元素均有唯一的“后继”;
  • 除第一个元素之外,其他元素均有唯一的“前驱”;

常见的线性结构有栈、队列、数组、字符串等。

2 顺序存储实现的线性表

2.1 结构定义

typedef int Status;
typedef int ElementType;
static int SUCCESS = 0;
static int ERROR = -1;
static int MAXSIZE = 100;

typedef struct {
    ElementType *data;
    int length;
} LinkList;

2.2 初始化

Status initLinkList(LinkList *l) {
    l->data = malloc(sizeof(ElementType) * MAXSIZE);
    if (l->data == NULL){
        return ERROR;
    }
    l->length = 0;
    return SUCCESS;
}

2.3 打印

Status traverseLinkList(LinkList l) {
    printf("LinkList: ");
    for (int i = 0; i < l.length; i++) {
        printf("%d ", l.data[i]);
    }
    printf("\n");
    return SUCCESS;
}

2.4 插入

Status insertIntoLinkList(LinkList *l, int i, ElementType e) {
    // 检查位置 i 是否合法
    if (i < 0 || i > l->length) {
        return ERROR;
    }
    // 检查线性表是否已满
    if (l->length >= MAXSIZE) {
        return ERROR;
    }
    // 移动 i 及后面的元素以插入新的元素
    if (i < l->length) {
        // 向后移动元素
        for (int j = l->length - 1; j >= i; j--) {
            l->data[j + 1] = l->data[j];
        }
    }
    // 赋值
    l->data[i] = e;
    // 修改长度
    l->length++;
    return SUCCESS;
}

2.5 删除

Status deleteFromLinkList(LinkList *l, int i, ElementType *e) {
    // 检查位置 i 是否合法
    if (i < 0 || i > l->length) {
        return ERROR;
    }
    // 返回被删除的元素
    *e = l->data[i];
    // 向前移动元素
    for (int j = i; j < l->length - 1; j++) {
        l->data[j] = l->data[j + 1];
    }
    // 修改长度
    l->length--;
    return SUCCESS;
}

2.6 清空

Status clearLinkList(LinkList *l) {
    // 只需将长度置为 0 即可,不需要抹除数据
    l->length = 0;
    return SUCCESS;
}

2.7 销毁

Status destroyLinkList(LinkList *l) {
    free(l);
    l = NULL;
    return SUCCESS;
}

其他获取元素、获取长度、判断为空等方法与上述方法类似,比较简单不再赘述。

2.8 使用

int main() {
    // 声明线性表
    LinkList l;
    
    printf("初始化线性表\n");
    initLinkList(&l);
    traverseLinkList(l);
    
    printf("线性表插入\n");
    for (int i = 0; i < 10; i++) {
        insertIntoLinkList(&l, i, i * 2);
    }
    traverseLinkList(l);
    
    printf("线性表插入 999\n");
    insertIntoLinkList(&l, 4, 999);
    traverseLinkList(l);

    ElementType e;
    deleteFromLinkList(&l, 5, &e);
    printf("线性表删除 %d\n", e);
    traverseLinkList(l);
    
    printf("线性表清空\n");
    clearLinkList(&l);
    traverseLinkList(l);

    printf("线性表销毁\n");
    destroyLinkList(&l);
  	return 0;
}

打印结果:

初始化线性表
LinkList: 
线性表插入
LinkList: 0 2 4 6 8 10 12 14 16 18 
线性表插入 999
LinkList: 0 2 4 6 999 8 10 12 14 16 18 
线性表删除 8
LinkList: 0 2 4 6 999 10 12 14 16 18 
线性表清空
LinkList: 
线性表销毁

3 单向链表

链式存储的特点是内存在物理层面上是不连续的,数据元素之间需要通过指针进行连接,构成逻辑意义上的线性表结构。

3.1 结构定义

typedef int Status;
typedef int ElementType;
static int SUCCESS = 0;
static int ERROR = -1;
/// 定义单向链表的节点
struct Node {
    ElementType data;   // 数据域
    struct Node *next;  // 指针域,指向逻辑中的下一个节点
};
typedef struct Node *LinkList;

3.2 初始化

/// 链表的初始化
Status initLinkList(LinkList *l) {
    // 创建一个头结点,作为哨兵节点
    *l = (LinkList)malloc(sizeof(struct Node));
    // 内存分配失败
    if (*l == NULL) {
        return ERROR;
    }
    // 置空指针域
    (*l)->next = NULL;
    return SUCCESS;
}

初始化中创建了一个头结点作为辅助哨兵节点,好处在于当插入或删除时,不需要考虑该节点是否是头结点,从而省去一些不必要的判断。

3.3 打印

/// 链表的遍历打印
Status traverseLinkList(LinkList l) {
    // 因为默认有头结点,所以从头结点的 next 开始
    LinkList p = l->next;
    printf("LinkList: ");
    while (p) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
    return SUCCESS;
}

3.4 插入

/// 单链表的插入
Status insertIntoLinkList(LinkList *l, int i, ElementType e) {\
    int j = 1;
    LinkList p = *l;
    // 遍历链表寻找插入位置
    while (p && j < i) {
        p = p->next;
        j++;
    }
    // 位置寻找错误
    if (!p || j > i) {
        return ERROR;
    }
    // 此时 p 为位置 i 的前一个节点
    // 创建一个新的节点
    LinkList s = (LinkList)malloc(sizeof(LinkList *));
    // 数据域赋值
    s->data = e;
    // 将 p 的后继赋值给新的节点
    s->next = p->next;
    // 将 s 作为 p 新的后继
    p->next = s;
    return SUCCESS;
}

3.5 取值

/// 链表的取值
Status getElementFromLinkList(LinkList l, int i, ElementType *e) {
    int j = 1;
    LinkList p = l->next;
    // 遍历链表寻找读取位置
    while (p && j < i) {
        p = p->next;
        j++;
    }
    if (!p || j > i) {
        return ERROR;
    }
    // 此时 p 为位置 i 上的节点
    *e = p->data;
    return SUCCESS;
}

寻找位置的方式和插入时类似,其中 while (p && j <= i) 的结束条件是 j == i,所以找到的是 i 上的节点,和插入时的 while (p && j < i) 位置不同。

3.6 删除

/// 删除单链表的节点
Status deleteFromLinkList(LinkList *l, int i, ElementType *e) {
    int j = 1;
    LinkList p = (*l)->next;
    // 遍历链表寻找删除位置
    while (p && j < (i - 1)) {
        p = p->next;
        j++;
    }
    // 位置寻找错误
    if (!p || j > (i - 1)) {
        return ERROR;
    }
    // 此时 p 为位置 i 的前一个节点
    // 获取被删除的节点
    LinkList s = p->next;
    // 数据返回
    *e = s->data;
    // 指针指向被删除d节点的下一个节点
    p->next = p->next->next;
    // 释放被删除的节点
    free(s);
    return SUCCESS;
}

3.7 完整调用

int testLinkList02() {
    // 节点声明
    LinkList l;
    
    // 初始化链表头结点
    if (initLinkList(&l) == SUCCESS) {
        printf("链表初始化成功\n");
    } else {
        printf("链表初始化失败\n");
    }
    
    printf("链表插入数据\n");
    for (int i = 0; i < 10; i++) {
        insertIntoLinkList(&l, 1, i);
    }
    traverseLinkList(l);
    
    ElementType e1;
    getElementFromLinkList(l, 3, &e1);
    printf("链表获取第3个数据 %d\n", e1);
    traverseLinkList(l);
    
    ElementType e2;
    deleteFromLinkList(&l, 4, &e2);
    printf("链表删除第4个数据 %d\n", e2);
    traverseLinkList(l);
        
    printf("清空链表\n");
    clearLinkList(&l);
    traverseLinkList(l);
    return 0;
}

打印结果:

链表初始化成功
链表插入数据
LinkList: 9 8 7 6 5 4 3 2 1 0 
链表获取第3个数据 7
LinkList: 9 8 7 6 5 4 3 2 1 0 
链表删除第4个数据 6
LinkList: 9 8 7 5 4 3 2 1 0 
清空链表
LinkList: 

3.8 头插法构建链表

/// 头插法构建链表
Status createLinkListByHead(LinkList *l, int n) {
    // 创建一个头节点作为辅助哨兵节点
    *l = (LinkList)malloc(sizeof(LinkList *));
    (*l)->next = NULL;
    // 循环插入 n 个节点
    LinkList p;
    for (int i = 0; i < n; i++) {
        // 创建新的节点并赋值
        p = (LinkList)malloc(sizeof(LinkList *));
        p->data = i;
        // 新的节点作为原首节点的前驱节点
        p->next = (*l)->next;
        // 重置头结点的后继为新节点
        (*l)->next = p;
    }
    return SUCCESS;
}
int main() {
    LinkList p;
    printf("尾插法插入数据\n");
    createLinkListByTail(&p, 5);
    traverseLinkList(p);
}

打印结果:

尾插法插入数据
LinkList: 0 1 2 3 4 

3.9 尾插法构建链表

/// 尾插法构建链表
Status createLinkListByTail(LinkList *l, int n) {
    // 创建一个头节点作为辅助哨兵节点
    *l = (LinkList)malloc(sizeof(LinkList *));
    (*l)->next = NULL;
    // 记录当前节点
    LinkList q = *l;
    LinkList p;
    // 循环插入 n 个节点
    for (int i = 0; i < n; i++) {
        // 创建新的节点并赋值
        p = (LinkList)malloc(sizeof(LinkList *));
        p->data = i;
        p->next = NULL;
        // 将新节点作为链表尾节点的后继
        q->next = p;
        // 重置尾节点
        q = p;
    }
    return SUCCESS;
}
int main() {
    LinkList p;
    printf("头插法插入数据\n");
    createLinkListByHead(&p, 5);
    traverseLinkList(p);
}

打印结果:

头插法插入数据
LinkList: 4 3 2 1 0