数据结构与算法 —— 表

一. 引言

表是一种线性数据结构,其中的元素按照一定的顺序排列。每个元素在表中有一个唯一的位置,称为索引(Index)。表中的元素可以是任意类型的数据,如整数、字符、字符串或其他复杂数据结构。

二. 表的基本概念

  • 元素(Element):在数据表中,每一个独立的数据项都被称作一个元素。
  • 序号(Index):为了标识和定位,表中的每一个元素都被赋予一个非负整数作为其在表中的位置标识,该整数通常从0开始计数。
  • 前驱(Predecessor):在数据表的序列中,如果元素a1位于元素a2之前,那么我们称a1为a2的前驱。
  • 后继(Successor):相对地,如果元素a3紧随元素a2之后出现在数据表中,那么a3就被称为a2的后继。

在表中,每个元素有唯一的前驱后继(第一个元素或最后一个元素除外)

三. 线性表

1.定义

线性表,顾名思义,是由相同数据类型的n(n≥0)个数据元素构成的有限序列。这里的n代表着线性表的长度,用以衡量线性表中所包含元素的数量。当n等于0时,线性表便成为了一个空表,意味着它不包含任何元素。

在描述线性表时,我们通常用字母L来表示,其结构可以表达为L=(a1, a2, …, an),其中每个元素ai都是线性表中的一个组成部分。这些元素属于相同的类型,因此它们在内存中所占用的空间大小一致。

线性表具有以下特性:

  • 有限性:线性表中的元素数量是有限的,不是无限延伸的。
  • 有序性:线性表中的元素按照一定的顺序排列,这种顺序是固定的,不是随机的。

在线性表中,每个元素ai都有一个明确的位序,用以标识其在表中的位置。位序是从1开始的,这意味着线性表的第一个元素a1具有位序1,而最后一个元素an则具有位序n。

具体来说,线性表的结构如下:

  • 表头元素:a1,它是线性表中的第一个元素,没有直接前驱。
  • 中间元素:除了a1之外,每个元素都有一个直接前驱,即每个元素除了第一个元素之外,都有一个在它之前的元素。
  • 表尾元素:an,它是线性表中的最后一个元素,没有直接后继。

这种有序排列的特性使得线性表成为数据处理和存储的基本结构之一,广泛应用于各种算法和数据结构的实现中。

2.特点

基于数组实现的线性表在进行以下操作时的时间复杂度为:

索引查找

O(1)

任意位置插入

O(n)

查找值

O(n)

删除

O(n)

从这个图中可以看出,对于顺序存储的线性表(如数组),访问、插入、删除等操作的时间复杂度通常较低,尤其是访问操作,时间复杂度为O(1)。这使得线性表在处理大量数据时具有较高的效率。

3.线性表的实现

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

typedef struct list
{
    //数据 - 数组
    int *data;
    //容量 - 数组容量
    int capacity;
    //大小
    int size;
}List;

void init(List *l, int n);
void add(List *l, int n);
void show(List *l);
void insert(List *l, int index, int n);
void incr(List *l);
int get(List *l, int index);//查找值
int find(List *l, int n);//查找索引
void delete(List *l, int index);
bool empty(List *l);
void clear(List *l);


int main(int argc, char const *argv[])
{
    List *li = malloc(sizeof(struct list));
    //初始化
    init(li, 3);
    //添加
    add(li, 100);
    add(li, 200);
    add(li, 300);
    add(li, 400);
    add(li, 500);

    add(li, 600);
    add(li, 700);
    show(li);
    insert(li, 3, 2000);
    show(li);
    get(li, 3);
    find(li, 300);

    delete(li, 5);
    show(li);
    empty(li);
    //显示
    show(li);
    clear(li);
    show(li);
    return 0;
}

void init(List *l, int n)
{
    l->capacity = n;
    l->size = 0;
    l->data = malloc(sizeof(int) * n);
}

void add(List *l, int n)
{
    if (l->size == l->capacity)
    {
        incr(l);
    }
    l->data[l->size] = n;
    l->size++;
}

void show(List *l)
{
    for(size_t i = 0; i < l->size; i++)
    {
        printf("[%ld] = %d,", i, l->data[i]);
    }
    printf("\n");
}

void insert(List *l, int index, int n)
{
    //边界
    if (index > l->size)
    {
        return ;
    }
    
    if (l->size == l->capacity)
    {
        incr(l);
    }
    for (size_t i = l->size - 1; i >= index; i--)
    {
        l->data[i + 1] = l->data[i];
    }
    l->data[index] = n;
    l->size++;
}

void incr(List *l)
{
    //增量
    // l->capacity *= 2;
    //l->data = realloc(l->data, sizeof(int) * l->capacity);
    int incr = l->capacity >> 1;
    //右移一位 变成一半
    l->capacity += incr;
    //扩容
    l->data = realloc(l->data, sizeof(int) * l->capacity); 
    printf("扩容至%d\n", l->capacity);
    
}

int get(List *l, int index)
{
    if (index > l->size)
    {
        return -1;
    }
    printf("%d 对应的值是%d\n",index, l->data[index]);
}

int find(List *l, int n)
{
    for (size_t i = 0; i < l->size; i++)
    {
        if (l->data[i] == n)
        {
            printf("%d对应的索引为%ld\n", n, i);
        }
    }

    
}

void delete(List *l, int index)
{
    if (index > l->size - 1)
    {
        printf("该索引不存在\n");
        return;
    }
    for (size_t i = index; i <= l->size - 1; i++)
    {
        l->data[i] = l->data[i + 1];
    }
    l->size--;
}
bool empty(List *l)
{
    if (l->size > 0)
    {
        printf("不是空的\n");
    }
    return true;
    
}
void clear(List *l)
{
    l->data = 0;
    free(l->data);
    l->size = 0;
}

这段代码实现了如何通过数组实现线性表的基本操作,包括添加、删除、查找、显示和初始化等。

功能:

  1. 初始化列表init 函数用于初始化列表,包括分配内存、设置列表的容量和当前大小。默认容量为 n,初始大小为0。

  2. 添加元素add 函数用于在列表末尾添加元素。如果列表已满,会调用 incr 函数进行扩容。每次添加后,列表大小增加1。

  3. 显示列表show 函数用于打印列表中的所有元素,按索引顺序显示。

  4. 插入元素insert 函数允许在指定索引处插入一个元素。如果列表已满,会进行扩容。插入操作会将列表中的元素向后移动以腾出空间。

  5. 扩容incr 函数通过增加列表容量的一半来实现扩容,然后重新分配内存以扩展列表的大小。

  6. 获取元素值get 函数用于获取指定索引处的元素值。如果索引超出范围,返回-1。

  7. 查找元素索引find 函数遍历列表,查找指定元素的索引,并打印结果。如果元素不存在,则不打印任何内容。

  8. 删除元素delete 函数允许删除指定索引处的元素。元素删除后,后面的元素会向前移动填充空位,列表大小减1。

  9. 检查列表是否为空empty 函数检查列表是否为空。如果列表大小大于0,则返回 false,表示列表不是空的。

  10. 清空列表clear 函数将列表的数据指针设置为0,释放当前分配的内存,并将列表大小设置为0,从而清空列表。

在 main 函数中,创建了一个列表实例并初始化了容量为3。然后,连续添加了6个元素,并显示了列表内容。接着,插入了一个元素,显示了更新后的列表。使用 get 函数获取了指定索引处的元素值,使用 find 函数查找了元素索引。删除了一个元素,再次显示了列表内容,并检查了列表是否为空。最后,清空了列表,并再次显示了空列表。

四.链表

1.定义

链表(Linked List)是一种线性数据结构,其中的数据元素不是直接存储在连续的内存位置,而是通过指针相互连接。每个数据元素称为节点,每个节点包含两部分:数据域(用来存储数据)和指针域(指向下一个节点的地址)。

链表的主要类型有以下几种:

  1. 单链表(Singly Linked List):每个节点包含一个数据元素和一个指向下一个节点的指针。单链表只能从头部开始遍历,因为每个节点只知道其下一个节点的位置。

  2. 双链表(Doubly Linked List):每个节点包含数据元素、一个指向下一个节点的指针和一个指向前一个节点的指针。双链表允许双向遍历,即能够从头部或尾部开始遍历。

  3. 循环链表(Circular Linked List):最后一个节点的指针指向链表的第一个节点,形成一个循环。这使得从任意节点开始都可以遍历整个链表。

2.特点

链表是一种灵活的存储结构,其关键特征在于数据元素以链的形式组织,无需连续的内存空间。这种设计使得链表能够动态分配存储空间,非常适合元素频繁插入和删除的情况。链表的每个节点包含数据和指向下一个节点的指针,形成一条链。

3.实现

1.单向链表

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


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

// typedef struct node* List;
typedef struct list
{
    Node* head;
    int size;
}List;

void init(List *l);
void add(List *l, int data);
void display(List *l);
void insert(List *l, int data, int index);
void delete(List *l, int index);
void find(List *l, int index);
void clear(List *l);
int size(List *l);


int main(int argc, char const *argv[])
{
    // Node *head = malloc(sizeof(Node));

    // Node n1;
    // Node n2;
    // Node n3;

    // n1.data = 100;
    // n2.data = 200;
    // n3.data = 300;

    // head->next = &n1;
    // n1.next = &n2;
    // n3.next = NULL;

    //创建
    List *link = malloc(sizeof(List));
    //初始化
    init(link);
    //添加
    add(link, 100);
    add(link, 200);
    add(link, 300);
    //显示
    display(link);
    delete(link,2);
    display(link);
    return 0;
}

void init(List *l)
{
    l->head = malloc(sizeof(Node));
    l->head->next = NULL;
    l->size = 0;
}

void add(List *l, int data)
{
    // Node node = {data, NULL};
    Node *node = malloc(sizeof(Node));
    node->data = data;
    node->next = NULL;

    if (l->head->next == NULL)
    {
        //链表为空链表
        l->head->next = node;
    }
    else
    {
        // //头插法
        // node->next = l->head->next;
        // l->head->next = node;
        //尾插法
        Node *n = l->head;
        while (n->next != NULL)
        {
            n = n->next;
        }
        n->next = node;
    }
    l->size++;
}

void display(List *l)
{
    Node *n = l->head->next;
    while (n != NULL)
    {
        printf("%d,", n->data);
        n = n->next; 
    }
    printf("\n");
}

void delete(List *l, int index)
{
    Node *prev = l->head;
    for (size_t i = 0; i < index; i++)
    {
        prev = prev->next;
    }
    Node *n = prev->next;
    prev->next = n->next;
    l->size--;
    free(n);
    
}

void insert(List *l, int data, int index)
{
    Node *prev = l->head;
    for (size_t i = 0; i < index; i++)
    {
        prev = prev->next;
    }

    Node *n = prev->next;
    Node *node = malloc(sizeof(Node));

    node->data = data;
    node->next = n;
    
    l->size++;
    free(n);
}
void find(List *l, int index);
void clear(List *l);
int size(List *l);

实现了如何通过链表结构在动态内存中存储数据,并实现基本的操作。链表相比于数组,更灵活地适应了元素频繁插入和删除的需求,但访问元素时需要遍历链表,时间复杂度为O(n)。

  •  初始化 (init)

初始化函数创建链表的头节点,并将其next指针设置为NULL。同时,初始化链表的大小为0。

  • 添加 (add)

添加函数接收一个整数值和链表指针作为参数。如果链表为空,直接将新节点添加到头节点的next指针处。如果链表非空,则在链表末尾添加新节点。

  • 显示 (display)

显示函数遍历链表,打印每个节点的数据。从头节点的next指针开始,直到链表末尾。

  • 删除 (delete)

删除函数接收一个索引作为参数。根据索引找到需要删除的节点的前一个节点,然后更新前一个节点的next指针,跳过被删除的节点。最后释放被删除节点的内存。

  • 插入 (insert)

插入函数接收一个整数值、链表指针和一个索引作为参数。根据索引找到插入位置的前一个节点,创建新节点,并将其插入到该位置。

  • 查找 (find)

查找函数接收一个索引作为参数。遍历链表,查找指定索引处的节点。如果找到,打印节点数据;否则,打印找不到的提示。

  •  清空 (clear)

清空函数释放链表中的所有节点内存,并将链表大小设置为0。

  • 获取链表大小 (size)

获取链表大小的函数返回链表中节点的数量。

  • 主函数 (main)

main函数中,首先创建了一个链表实例,并初始化了头节点和链表大小。然后,连续添加了三个元素,并显示了链表内容。接着,删除了索引为2的元素,并再次显示了链表内容。

2.双向链表

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

typedef struct node
{
    int data;
    struct node *prev;//前驱
    struct node *next;//后继
}Node;

//一个指针
//typedef struct node link;

//两个指针
typedef struct list
{
    int size;//大小
    struct node *head;//头指针
    struct node *tail;//尾指针
}List;

void init(List *l);
void add(List *l, int value);
void display(List *l);
void insert(List *l, int index, int value);
void delete(List *l, int index);
int get(List *l, int index);
int find(List *l, int value);
int size(List *l);
void clear(List *l);


int main(int argc, char const *argv[])
{
    List *l = malloc(sizeof(Node));
    init(l);
    add(l,1000);
    add(l,2000);
    add(l,8888);
    add(l,3000);
    add(l,8888);
    add(l,8888);

    display(l);

    insert(l,2,1234);

    display(l);
    
    delete(l,1);
    display(l);
    get(l,3);
    find(l, 8888);
    size(l);
    clear(l);
    display(l);
    return 0;
}


void init(List *l)
{
    l->size = 0;
    l->head = malloc(sizeof(Node));
    l->tail = malloc(sizeof(Node));
    l->head->next = l->tail;
    l->tail->prev = l->head;
}

void add(List *l, int value)
{
    Node *n = malloc(sizeof(Node));
    n->data = value;

    n->prev = l->tail->prev;
    l->tail->prev->next = n;

    n->next = l->tail;
    l->tail->prev = n;
    
    l->size++;
}

void display(List *l)
{
    Node *n = malloc(sizeof(Node));
    n = l->head;
    for (size_t i = 0; i < l->size; i++)
    {
        n = n->next;
        printf("[%ld] = %d,", i, n->data);
    }
    printf("\n");
    
}
void insert(List *l, int index, int value)
{
    if (index < 0 || index > l->size)
    {
        printf("索引错误\n");
        return ;
    }
    
    Node *n = malloc(sizeof(Node));
    n->data = value;
    if (l->size == 0)
    {
        n->prev = l->head;
        l->head->next = n;

        n->next = l->tail;
        l->tail->prev = n;
    }
    else if(l->size == index)
    {
        n->prev = l->tail->prev;
        l->tail->prev->next = n;

        n->next = l->tail;
        l->tail->prev = n;
    }
    else
    {
        Node *newnode = malloc(sizeof(Node));
        newnode = l->head->next;
        for (size_t i = 0; i < index; i++)
        {
            newnode = newnode->next;
        }
        n->prev = newnode->prev;
        newnode->prev->next = n;

        n->next = newnode;
        newnode->prev = n;
        
    }
    l->size++;
}

void delete(List *l, int index)
{
    Node *newnode = malloc(sizeof(Node));
    newnode = l->head->next;
    for (size_t i = 0; i < index; i++)
    {
        newnode = newnode->next;
    }
    newnode->prev->next = newnode->next;
    newnode->next->prev = newnode->prev;
    l->size--; 
    free(newnode);
}

int get(List *l, int index)
{
    Node *newnode = malloc(sizeof(Node));
    newnode = l->head->next;
    for (size_t i = 0; i < index; i++)
    {
        newnode = newnode->next;
    }
    printf("[%d] = %d\n", index, newnode->data);
    return 0;
}

int find(List *l, int value)
{
    Node *newnode = malloc(sizeof(Node));
    newnode = l->head;
    for (size_t i = 0; i < l->size; i++)
    {
        newnode = newnode->next;
        if (newnode->data == value)
        {
            printf("%d对应的索引为%ld\n", value, i);
        }
    }
    return 0;
}
int size(List *l)
{
    printf("size = %d\n", l->size);
    return 0;
}
void clear(List *l)
{
    l->size = 0;
    free(l->head);
}

实现了如何通过双向链表结构在动态内存中存储数据,并实现基本的操作。双向链表相比于单向链表,更灵活地适应了元素频繁插入和删除的需求,但访问元素时需要遍历链表,时间复杂度为O(n)。

  • 1初始化 (init)

初始化函数创建链表的头节点和尾节点,并将它们相互连接。同时,初始化链表的大小为0。

  •  添加 (add)

添加函数接收一个整数值和链表指针作为参数。创建一个新节点,并将其插入到链表的末尾。更新相关节点的prev和next指针,以保持链表的完整性。

  •  显示 (display)

显示函数遍历链表,打印每个节点的数据。从头节点的next指针开始,直到链表末尾。

  • 插入 (insert)

插入函数接收一个整数值、链表指针和一个索引作为参数。根据索引找到插入位置的前一个节点,创建新节点,并将其插入到该位置。更新相关节点的prev和next指针,以保持链表的完整性。

  •  删除 (delete)

删除函数接收一个索引作为参数。根据索引找到需要删除的节点,更新前一个节点和后一个节点的prev和next指针,跳过被删除的节点。最后释放被删除节点的内存。

  •  获取 (get)

获取函数接收一个索引作为参数。遍历链表,查找指定索引处的节点。如果找到,打印节点数据;否则,打印找不到的提示。

  • 查找 (find)

查找函数接收一个整数值作为参数。遍历链表,查找包含该值的节点。如果找到,打印节点索引;否则,打印找不到的提示。

  • 获取链表大小 (size)

获取链表大小的函数返回链表中节点的数量。

  • 清空 (clear)

清空函数释放链表中的所有节点内存,并将链表大小设置为0。

  • 主函数 (main)

在main函数中,首先创建了一个链表实例,并初始化了头节点和尾节点。然后,连续添加了多个元素,并显示了链表内容。接着,插入了索引为2的元素,删除了索引为1的元素,并再次显示了链表内容。使用get函数获取了指定索引处的元素值,使用find函数查找了元素索引。最后,清空了链表,并再次显示了空链表。

3.循环链表

循环链表,顾名思义就是就是是一个环,比如:

在单向链表中,链表的最后一个元素的next指向表头head,这样就形成了一个环;

而在双向链表中,就是表头head 的前驱prev 是表尾tail,而表尾的tail的后继next是表头head就形成了一个环。

经典题目

在循环链表中有一个很经典的题目,如何判断一个链表中是否有环?

这个题目可以使用到快慢指针法,顾名思义,就是一个指针移动快,一个移动慢,如果链表中有环,那么最后这个快指针一定会追上慢指针,这就是快慢指针法。

代码实现:

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

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

// 判断链表是否有环
int hasCycle(Node *head) {
    if (head == NULL || head->next == NULL) {
        return 0; // 链表为空或只有一个节点,不可能有环
    }

    Node *slow = head;
    Node *fast = head->next;

    while (slow != fast) {
        if (fast == NULL || fast->next == NULL) {
            return 0; // 快指针到达链表末尾,没有环
        }
        slow = slow->next;          // 慢指针每次移动一步
        fast = fast->next->next;    // 快指针每次移动两步
    }

    return 1; // 快指针追上慢指针,说明有环
}

// 创建新节点
Node* createNode(int data) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

int main() {
    // 创建一个有环的链表
    Node *head = createNode(1);
    head->next = createNode(2);
    head->next->next = createNode(3);
    head->next->next->next = createNode(4);
    head->next->next->next->next = head->next; // 创建环

    if (hasCycle(head)) {
        printf("链表中有环\\n");
    } else {
        printf("链表中没有环\\n");
    }

    return 0;
}

通过上述代码,我们可以判断一个链表中是否存在环。如果有环,快指针最终会追上慢指针;如果没有环,快指针会到达链表末尾。

五.总结

表作为一种基础的数据结构,其独特的结构特性以及相应的操作,堪称众多复杂算法的关键基石。线性表在需要实现快速随机访问的场景之中,具有十分显著的优势。相比之下,链表在频繁进行插入和删除操作的情形下,则展现出更为卓越的性能表现。通过对表进行深入透彻的理解,并切实地进行代码层面的实现,能够为后续进一步深入学习以及开发更为复杂的数据结构和算法,筑牢坚实稳固的根基。

表的结构和操作看似简单,实则蕴含着深刻的逻辑和价值。线性表以其连续存储的方式,使得随机访问变得高效快捷,能够在瞬间定位到特定位置的元素。而链表则凭借其灵活的节点连接方式,在插入和删除操作时无需进行大量的数据移动,极大地提高了操作效率。对这些特性的深入把握,不仅有助于我们在实际编程中选择合适的数据结构,更能够为我们探索更高级的数据结构和算法提供坚实的理论支持和实践经验。

本文章到这里就结束了,下篇文章会在介绍一些表的另外两个非常重要的实例,栈和队列。

本文章若有问题,欢迎大家批评指正。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值