数据结构之线性表

数据结构之线性表



前言

首先简单介绍一下什么是数据结构。

顾名思义,数据结构必然由一些数据组成,以某种方式形成一种成体系的结构,就像分子,原子,都是按特定方式排列的。
在计算机中,数据也不是孤立无援、杂乱无须的,而是具有内在联系的数据信息集合,数据之间的特定关系,也就是数据的组织形式。


一、线性表是什么?

线性表是一种常见的数据结构,它是具有相同数据类型的 n 个数据元素的有限序列。在线性表中,元素之间存在一对一的线性关系,即除了第一个和最后一个元素之外,每个元素都有一个前驱元素和一个后继元素。线性表中的元素可以是基本数据类型,也可以是复杂的数据结构。

以上是ChatGPT的解释,过于晦涩难懂,直接丢掉。接下来看我的:

线性表,从名字上你就能感觉到,这是一种具有线一样性质的表。幼儿园排成一排过马路的小朋友,地上列成一队觅食的蚂蚁,又或者是排队领鸡蛋的大妈,都可以视作一种线性表。

在上面几种举例中,小朋友应该难以背下来整排人的顺序,中间的蚂蚁不知道打头的是不是小花小明,排队的大妈也不一定和队尾的阿姨一起跳过舞。每个个体只知道自己前面是谁、后面是谁,就好像有一条无形的线将他们串联起来,就可以称之线性表。

这里有两个地方需要注意:

  • 线性表元素个数有限
  • 表内元素具有顺序

这两点不难理解,每个班的小朋友终究是有限的,如果在排队过程中,某两个小朋友打了起来因而乱作一团,显然也不符合线性表的定义。而那种无限长的表,大概只存在于数学概念中。

若将线性表记为(a1, …,a日,ai,a1+1, …,an), 则表中3i-1领先于a1,a,领先于a1+1,称3i-1是a1的直接前驱元素,ai+1是ai的直接后继元素。 当i=l, 2, …, n-1时,ai有且仅有一个直接后继, 当i=2, 3, …, n时,ai有且仅有一个直接前驱。
线性表
再举一个例子,你的老师,你老师的老师,直至追溯到孔子之一类祖师级别的人物,构不构成线性表呢?

显然,你的老师不只有你一个学生,孔子也教出了七十二贤士。换句话说,表上的每个节点不止有一个后驱节点。所以这种情况是不应该归类为线性表的。

二、线性表操作

咱们暂且将怎么创建表放一放,直接介绍线性表的整体操作。
还是小朋友过马路的情景:

  • 小朋友A要去拉臭臭,离开队伍;
  • 其他班小朋友B看见队里有自己的朋友,插了进来;
  • 结果B发现还是太远了,于是商量着和后面的小朋友交换位置;
  • 老师看小朋友比较累了,宣布原地解散,小朋友四散跑开… …

当应用场景不同时,线性表要进行相应的操作,才能使这种结构的存在有意义。
下面用代码列举一些常用的操作:

ADT 线性表(List)
Operation
	InitList(*L):初始化操作,建立空表
	ListEmpty(L): 若List为空,返回true
	ClearList(*L): 将线性表清空。
	GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。		
	 LocateElem(L,e):在线性表L中查找与给定值e相等的元素,
	 				如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
	 ListInsert(*L,i,e):在线性表L中的第i个位置插入新元素 e。
	 ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。
	 ListLength(L):返回线性表L的元素个数。

比如,要实现两个线性表集合A和B的并集操作。即要使得集合A=AUB。说白了,就是把存在集合B中但并不存在A中的数据元素插入到A中即可。仔细分析一下这个操作,发现我们只要循环集合B中的每个元素,判断当前元素是否存在A中,若不存在,则插入到A中即可。思路应该是很容易想到的。

三、线性表的结构

有些同学看到这里可能懵懵的,你跟我说屋里呱啦一大堆,到底咋定义出一个线性表啊,话不宜迟,我们现在就开始介绍:

根据前文的内容,相信你已经了解了线性表节点结构的关键,我将它提炼一下:

  • 节点的数据
  • 后驱节点的信息

作为一种数据结构,存储数据是最基本的功能,这毋容置疑。那么这种线性结构应该怎么实现串联呢?前人为我们提出了解决方案。
我们只需要在每个节点中定义两个信息——数据和指向下一个节点的指针。就像下面这样:

struct Node {
    int data;
    struct Node *next;
};

一开始,我们先定义了一个结构体 Node,其中包含两个成员:data 用于存储节点的数据,next 是一个指向下一个节点的指针。这样,我们就能够通过这个 next 指针,将各个节点串联在一起,形成一个线性表。

接下来,我们可以使用这个节点结构来构建一个简单的线性表。例如,我们可以创建一个头节点,然后逐个添加节点,每个节点的 next 指针指向下一个节点,最后一个节点的 next 指向空(NULL),表示链表的结束。
接下来,我们将深入了解线性表的不同实现方式以及一些基本的操作。

1. 遍历链表

void traverseList(struct LinkedList *list) {
    struct Node *current = list->head;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

这个函数用于遍历链表并输出每个节点的数据。我们从头节点开始,逐个访问节点,并打印节点的数据。

2. 查找节点

struct Node* searchNode(struct LinkedList *list, int target) {
    struct Node *current = list->head;
    while (current != NULL) {
        if (current->data == target) {
            return current; // 找到目标节点
        }
        current = current->next;
    }
    return NULL; // 未找到目标节点
}

这个函数用于在链表中查找包含特定数据的节点。如果找到了目标节点,则返回该节点的指针,否则返回 NULL

3. 删除节点

void deleteNode(struct LinkedList *list, int target) {
    struct Node *current = list->head;
    struct Node *prev = NULL;

    // 查找目标节点,并记录前一个节点
    while (current != NULL && current->data != target) {
        prev = current;
        current = current->next;
    }

    if (current == NULL) {
        // 未找到目标节点
        return;
    }

    if (prev == NULL) {
        // 目标节点是头节点
        list->head = current->next;
    } else {
        // 目标节点不是头节点
        prev->next = current->next;
    }

    free(current); // 释放目标节点内存
}

这个函数用于删除链表中包含特定数据的节点。首先,我们查找目标节点,并记录目标节点的前一个节点。然后,根据目标节点在链表中的位置,更新前一个节点的 next 指针,最后释放目标节点的内存。

4. 插入节点

void insertNode(struct LinkedList *list, int newData, int position) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = newData;

    if (position == 0) {
        // 在头部插入节点
        newNode->next = list->head;
        list->head = newNode;
    } else {
        // 在指定位置后插入节点
        struct Node *current = list->head;
        for (int i = 0; i < position - 1 && current != NULL; i++) {
            current = current->next;
        }

        if (current == NULL) {
            // 位置超出链表长度,插入失败
            free(newNode);
            return;
        }

        newNode->next = current->next;
        current->next = newNode;
    }
}

这个函数用于在链表的指定位置插入一个新节点。如果位置是 0,则在头部插入节点;否则,在指定位置后插入节点。需要注意的是,如果指定的位置超出链表的长度,插入操作将失败。

以上是单链表的一些基本操作。当然,单链表还有其他操作,如反转链表、合并链表等,这些操作可以根据实际需求进行实现。

接下来,我们将讨论线性表的另一种实现方式——顺序表。

四、顺序表

有了前面的基础,咱们再来看一下顺序表这一部分。

顺序表是一种用一组地址连续的存储单元依次存储线性表中的元素的数据结构。它在内存中占用一块连续的存储空间,可以通过元素在内存中的相对位置来访问元素。顺序表适用于元素数量相对固定的情况。

先直接上代码

1. 初始化顺序表

#define MAX_SIZE 100

struct ArrayList {
    int data[MAX_SIZE];
    int length;
};

顺序表的初始化非常简单,我们使用一个数组来存储元素,并用 length 记录当前表中元素的个数。

2. 遍历顺序表

void traverseList(struct ArrayList *list) {
    for (int i = 0; i < list->length; i++) {
        printf("%d ", list->data[i]);
    }
    printf("\n");
}

遍历顺序表同样简单,我们使用一个循环来访问数组中的每个元素,并输出其值。

3. 查找元素

int searchElement(struct ArrayList *list, int target) {
    for (int i = 0; i < list->length; i++) {
        if (list->data[i] == target) {
            return i; // 找到目标元素,返回索引
        }
    }
    return -1; // 未找到目标元素
}

查找元素的操作也是通过循环遍历数组,找到目标元素时返回其索引。如果未找到目标元素,则返回 -1。

4. 删除元素

void deleteElement(struct ArrayList *list, int target) {
    int index = searchElement(list, target);

    if (index == -1) {
        // 未找到目标元素
        return;
    }

    // 将目标元素后面的元素向前移动
    for (int i = index; i < list->length - 1; i++) {
        list->data[i] = list->data[i + 1];
    }

    list->length--; // 更新顺序表长度
}

删除元素的操作首先需要找到目标元素的索引,然后将该索引后面的元素向前移动,最后更新顺序表的长度。

5. 插入元素

void insertElement(struct ArrayList *list, int newData, int position) {
    if (position < 0 || position > list->length) {
        // 插入位置无效
        return;
    }

    // 将插入位置后面的元素向后移动
    for (int i = list->length - 1; i >= position; i--) {
        list->data[i + 1] = list->data[i];
    }

    list->data[position] = newData; // 插入新元素
    list->length++; // 更新顺序表长度
}

插入元素的操作首先判断插入位置的有效性,然后将插入位置后面的元素向后移动,最后在插入位置处放入新元素并更新顺序表的长度。

双向链表:撞南墙就知道回头的顺序表

回忆一下,单向链表就像是一群有序排队的小伙伴,每个人都知道前一个,也知道后一个。但有时候,想要从后往前走,单向链表就有点捉襟见肘。

双向链表为我们提供了解决之道。在每个节点中,我们不仅保存了下一个节点的地址,还额外记录了前一个节点的地址。这样,我们就可以轻松实现前后遨游。

双向链表的节点可不能只有一个指针,而是需要两个。分别是 next 和 prev,分别代表下一个和上一个。

    +-----+    +-----+    +-----+    +-----+
    |  5  |<-->|  8  |<-->|  3  |<-->|  1  |
    +-----+    +-----+    +-----+    +-----+
      ↑          ↑          ↑          ↑
     prev        prev       prev       prev

双向链表的优势在于灵活性。在某些情况下,我们可能需要在链表中间插入或删除节点,这时候双向链表就能事半功倍。因为每个节点不仅知道自己的前后,也能迅速找到前后节点。
复杂度
查找: 由于每个节点都有 next 和 prev,查找时可以选择前进或后退,时间复杂度为 O(n/2),仍为 O(n)。
插入和删除: 可以在 O(1) 的时间内完成,只需要修改前后节点的指针。

双向链表的进阶应用

1. 双向链表的逆序输出:

逆序输出是一个常见而有趣的应用。通过利用 prev 指针,我们可以从链表的尾部开始遍历,实现逆序输出。

// 逆序输出
Node* current = tail;
while (current != NULL) {
    printf("%d ", current->data);
    current = current->prev;
}

2. 双向链表的循环应用:

循环遍历链表时,我们可以让链表的头尾相连,形成一个环,以便更灵活地处理循环操作。

// 形成循环
head->prev = tail;
tail->next = head;

3. 双向链表的删除操作:

在双向链表中,由于我们知道前后节点的位置,可以在 O(1) 时间内完成删除操作。

// 删除节点
Node* toDelete = current->next;
current->next = toDelete->next;
toDelete->next->prev = current;
free(toDelete); // 释放内存

4. 使用场景扩展:

  • 当需要频繁在链表两端插入或删除节点时,双向链表的表现更胜一筹。
  • 实现循环遍历、逆序输出等场景时,双向链表能事半功倍。

总结

以上就是线性表中链表的相关知识整理,配合代码是实例讲解链表的基本操作,相对于顺序表来说,链表的分类较为多一些,且操作也较为复杂一些,但只要认真看完本篇文章,你会对链表有更为详细的认知和理解,若有疏漏之处请指出,我也会持续学习。


									🎨觉得不错的话记得点赞收藏呀!!🎨

									    	😀别忘了给我关注~~😀
  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值