线性链式表

定义

线性链式表就是我们常说的链表,也被称为线性表的非顺序映像或链式映像。它与线性顺序表的最大区别就是各个存储单元是非连续的。它无法通过数学公式来确定每个存储单元在内存中的关系。链表中最重要的两个概念就是结点(Node)和next指针。在单向链表中结点通常只包含两个存储域,一个是data数据域,用来存储结点所代表的值。第二个是,next指针域,用来存储结点的后继。因此next指针成了寻找结点后继的唯一途径,也就是只有知道头结点,才能通过next指针一个个往下寻找。所以链表的结构是链式的,而非顺序的,他们在内存结构中往往是离散的。

分类

  • 按照结点中指针域的个数分类有两种: 单向链表双向链表

单向链表就是一个结点只包含一个next指针域,即一个结点只知道它的后继结点是谁。遍历链表的时候,只能从前往后遍历。

单链表

双向链表就是一个结点包含pre指针域和next指针域,即一个结点即知道它的后继结点,也知道它的前驱结点。遍历链表的时候,可以从前往后,也可以从后往前。但是从后往前。从前往后需要head指针开始,从后往前需要tail指针开始。

双链表

两者区别: 结点的前驱和后继是否都知道,遍历的时候是否支持双向遍历。

  • 按照存储方式分类有两种: 动态链表静态链表

动态链表就是平常我们最常见的链表,链表的插入,删除,建立都是动态执行的。没有内存空间的预申请以及浪费。通常结点的定义方式就是包含data和next指针的结构体。当需要新建结点插入的时候通常用malloc申请新的结点内存空间,进行初始化数据域,修改指针域插入链表。当需要删除结点的时候,往往先修改链表的指针域,然后free结点。因此动态链表的元素容量不是固定的,是根据需求变化的。

单链表

静态链表是给有些不支持指针的编程语言实现链表的一种方式,通常用数组来实现。数组的每个分量是一个复合类型数据(结构体或者说类Class)。每个分量都有两个存储域,一个是data数据域,第二个next数据域,是保存当前分量的后继结点位于数组中的第几个分量。通常静态量插入,删除,建立都需要一个备用空间链表来管理内存空间的复用。插入的时候,需要从备用空间链表中申请复用可以复用的结点。如果备用空间链表没有可以复用的结点,往往把数组第一个没有存储任何数据的分量作为下一个结点使用。删除的时候,要将删除的结点放入备用空间链表,以提供复用。静态链表的容量是预申请的,是固定的。

两者区别: 动态链表存储空间不是预申请,容量不固定。静态链表存储空间是预申请的,容量通常在申请数组大小的就确定下来了。

静态链表

  • 按照末尾结点指针域是否为头结点分类: 循环链表非循环链表

循环链表 即链表最后一个结点的next指针域为头结点,构成环状结构。通常遍历链表的结束状态谓词不能用结点!=NULL来确定,而是应该用结点的后继结点!=HEAD结点。

循环链表

非循环链表 即链表最后一个结点的next指针域为NULL,不构成环状结构。遍历链表的结束状态谓词用结点!=NULL来确定。

两者区别: 末尾结点的next指针域是否指向头结点。

相关操作以及时间复杂度分析

索引(Index)

由于链表每个存储单元不是连续的,也没有任何数学公式可以表示,因此索引操作只能通过遍历来获取。遍历通常用next指针域来找到对应结点, 假设索引链表的第5个数据,通常要从头结点开始,经过for(int i=0; i<5; i++) p = p->next,到达第五个结点,然后返回对应的值(return p->data;)。现在假设i结点被索引的概率为pi。则索引操作的数学期望值为:

现在假设pi都具有相同的概率1/n; 那么对上述表达式进行化简得到如下结果:

再将如上级数进行整理可得

因此索引的时间复杂度为O(n)。

查找(find)

因为在链表中查找也是需要通过遍历的, 它在遍历的过程和索引操作是一样的,只不过循环的终止条件是是否查找到对应的值,而非需要走到位置的次数。现在假设需要寻找的值在结点i的概率为pi。则查找操作的数学期望值为:

现在假设pi都具有相同的概率1/n; 那么对上述表达式进行化简得到如下结果:

再将如上级数进行整理可得

因此查找的时间复杂度为O(n)。

插入(Insert)

与顺序表相比,链表的最大特点就在于插入与删除的方便。它不需要将插入位置之后的元素全部往后移动一个位置,只需要将插入结点的next指针域指向待插入位置之后的结点,将待插入位置之前的结点的next指针域指向插入结点就完成了插入。例如,以下链表 A->B->C中,将D插入B,C之间,只要进行如下操作:D->next = C, B->next = D, 就完成了插入,形成了A->B->D->C。
因此已经定位到插入位置的前提下链表插入的时间复杂度为O(1); 但是只给定在插入到第2个元素之前,还需要进行定位操作,定位操作就是索引操作的,因此时间复杂度是O(n)。
所以,如果没有定位到插入位置,链表插入操作的时间复杂度还是O(n)。

删除(Delete)

删除操作也是链表的一个优势,它不需要将删除位置以后的所有元素往前移动一个位置,只需要将删除位置之前的结点的next指针域指向删除位置之后的结点,然后free掉删除的结点即可。例如,以下链表 A->B->C中,将B删除,只要进行如下操作:A->next = C; free(B);就完成了删除,形成了A->C。
因此已经定位到删除位置的前提下链表删除的时间复杂度为O(1); 但是只给定在删除第2个元素,还需要进行定位操作,定位操作就是索引操作的,因此时间复杂度是O(n)。
所以,如果没有定位删除位置,链表删除操作的时间复杂度还是O(n)。

合并(Merge)

链表合并操作的算法和线性顺序表的合并操作的算法是一样的,时间复杂度也是一样的,都是O(m+n)。只不过区别于线性顺序表的合并,链表不需要申请存放合并之后线性表的空间,而是只要修改各个节点next指针域,使得通过结点“打散-重组”的方式进行合并,因此空间复杂度明显比线性表要低。相关的合并算法见我的另一篇博文《线性顺序表》

实现代码

动态单向链表

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

typedef struct LNode *PNode;

typedef struct LNode{
    int data;
    PNode next;
}LNode, *PNode;

/*
 * 从后往前建立链表
 */
void create_list(PNode *head, int n)
{
    *head = (PNode) malloc(sizeof(LNode));
    (*head)->data = n;
    (*head)->next = NULL;
    for(int i=n; i>0; i--){
        PNode node = (PNode) malloc(sizeof(LNode));
        scanf("%d", &node->data);
        node->next = (*head)->next;
        (*head)->next = node;
    }
}

/*
 * 在对应下标位置插入新的结点
 */
void insert(PNode head, int index, int newValue)
{
    PNode p = head;

    for(int i=0; i< index && p; i++)
        p = p->next;
    PNode newNode = (PNode)malloc(sizeof(LNode));
    newNode->data = newValue;
    newNode->next = p->next;
    p->next = newNode;
    head->data++;
}

/*
 * 删除对应下标的链表结点
 */
int deleteElement(PNode head, int index)
{
    PNode p = head;
    for(int i=0; i < index && p; i++)
        p = p->next;
    PNode deleteNode = p->next;
    p->next = deleteNode->next;
    int deleteValue = deleteNode->data;
    free(deleteNode);   // 释放结点内存
    head->data--;
    return deleteValue;
}


/*
 * 获取对应下标元素值
 */
int getNodeValue(PNode head, int index)
{
    PNode p = head;
    for(int i=0; i <= index; i++)
        p = p->next;
    return p->data;
}

/*
 * 合并链表
 */
void merge_list(PNode head1, PNode head2, PNode *rs)
{
    PNode p1 = head1->next;
    PNode p2 = head2->next;
    *rs = (PNode) malloc(sizeof(LNode));
    (*rs)->next = NULL;
    (*rs)->data = head1->data + head2->data;
    PNode prs = (*rs);

    while(p1&&p2){
        if(p1->data <= p2->data){
            prs->next = p1;
            p1 = p1->next;
            prs = prs->next;
        }else{
            prs->next = p2;
            p2 = p2->next;
            prs = prs->next;
        }
    }

    while(p1) {
        prs->next = p1;
        p1 = p1->next;
        prs = prs->next;
    }

    while(p2) {
        prs->next = p2;
        p2 = p2->next;
        prs = prs->next;
    }

    free(head1);
    free(head2);
}

/*
 * 打印链表信息,包含链表长度,元素内容
 */
void print_list_info(PNode head)
{
    PNode p = head;
    printf("The length of linked list is %d\nData: ", head->data);
    while(p->next) {
        p = p->next;
        printf("%d ", p->data);
    }
    printf("\n");
}

int main()
{

    PNode head;
    create_list(&head, 10);
    print_list_info(head);
    insert(head, 0, 100);
    print_list_info(head);

    PNode head1;
    PNode head2;
    PNode rs;

    create_list(&head1, 5);
    create_list(&head2, 3);
    merge_list(head1, head2, &rs);
    print_list_info(rs);

    return 0;
}

静态单向链表

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

#define MAX_SIZE 100 // 链表的最大长度
#define END -1

typedef struct {
    int data;
    int next;
} SLNode, SLinkedList[MAX_SIZE];


void create_slist(SLinkedList slist, int n)
{
    slist[0].data = n; // 头结点保存链表长度信息
    slist[0].next = 1;

    // 注意这里是从1开始的,不是0. 0用来作为链表的头结点了
    for(int i=1; i<=n; i++){
        scanf("%d", &slist[i].data);
        slist[i].next = i+1;
    }
    slist[n].next = END; // 链表最后一个结点的next为END(-1)
}

void init_backup_list(SLinkedList backupList)
{
    backupList[0].data = 0;
    backupList[0].next = END;
}

/*
 * 从备用空间链表中重用结点空间
 */
int malloc_node(SLinkedList backupList)
{
    int m_node = backupList[0].next; // 备用空间链表的第一个结点
    if(m_node != END) backupList[0].next = backupList[m_node].next; // 如果m_node不为END,也就是说备用空间链表的第一个结点可以被重用,则删除第一个结点,提供重用
    return m_node;
}

/*
 * 将删除的结点移入备用空间链表中
 */
void free_node(SLinkedList backupList, int index)
{
    backupList[index].next = backupList[0].next;
    backupList[0].next = index;
}

void insert(SLinkedList slist, SLinkedList backupList, int index, int value)
{
    int p = 0;
    for(int i=0; i < index; i++){
        p = slist[p].next;
    }

    // 先从备用链表中查看是否有可以重用的结点
    int insert_p = malloc_node(backupList);
    insert_p = insert_p == END ? slist[0].data+1 : insert_p; // 若insert_p为END说明没有重用的结点,则从链表空间中取下一个新的结点作为插入结点

    // 插入结点
    slist[insert_p].data = value;
    slist[insert_p].next = slist[p].next;
    slist[p].next = insert_p;

    slist[0].data++;
}

void deleteElement(SLinkedList slist, SLinkedList backupList, int index)
{
    int p = 0;
    for(int i=0; i < index; i++){
        p = slist[p].next;
    }

    slist[p].next = slist[slist[p].next].next;
    free_node(backupList, index);
}

int getElement(SLinkedList slist, int index)
{
    int p = 0;
    for(int i=0; i <= index; i++){
        p = slist[p].next;
    }
    return slist[p].data;
}


void print_slit_info(SLinkedList slist)
{
    printf("The length of the static linked list is %d\nData: ", slist[0].data);
    int p = slist[0].next;
    while(p!=END){
        printf("%d ", slist[p].data);
        p = slist[p].next;
    }
    printf("\n");
}

int main()
{
    SLinkedList slist;
    SLinkedList backupList;
    init_backup_list(backupList);
    create_slist(slist, 5);
    print_slit_info(slist);
    insert(slist, backupList, 2, 10);
    print_slit_info(slist);
    deleteElement(slist, backupList, 2);
    print_slit_info(slist);

    return 0;
}
动态双向链表
#include<stdio.h>
#include<stdlib.h>

typedef struct DLNode *PDLNode;

typedef struct DLNode{
    int data;
    PDLNode pre;
    PDLNode next;
}DLNode, *PDLNode;

void create_list(PDLNode *head, PDLNode *tail, int n)
{
    *head = (PDLNode) malloc(sizeof(DLNode));
    *tail = (PDLNode) malloc(sizeof(DLNode));

    (*head)->pre = NULL;
    (*head)->next = (*tail);
    (*head)->data = n;

    (*tail)->pre = (*head);
    (*tail)->next = NULL;
    (*tail)->data = n;

    for(int i=0; i<n; i++){
        PDLNode node = (PDLNode) malloc(sizeof(DLNode));
        scanf("%d", &node->data);
        node->pre = (*tail)->pre;
        (*tail)->pre->next = node;
        node->next = (*tail);
        (*tail)->pre = node;
    }


}

void insert(PDLNode head, int index, int value)
{
    PDLNode p = head;
    for(int i=0; i<index; i++)
        p = p->next;
    PDLNode node = (PDLNode)malloc(sizeof(DLNode));
    node->data = value;

    // 插入结点
    node->pre = p;
    p->next->pre = node;
    node->next = p->next;
    p->next = node;
}

void deleteElement(PDLNode head, int index)
{
    PDLNode p = head;
    for(int i=0; i<index; i++)
        p = p->next;
    PDLNode deleteNode = p->next;
    deleteNode->next->pre = p;
    p->next = deleteNode->next;
    free(deleteNode);
}

int getElement(PDLNode head, int index)
{
    PDLNode p = head->next;
    for(int i=0; i<index; i++)
        p = p->next;
    return p->data;
}

void print_dlist_info(PDLNode head)
{
    printf("The length of the doubly linked list is %d\nData: ", head->data);
    PDLNode p = head->next;
    while(p->next){
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

void print_dlist_info_reverse(PDLNode tail)
{
    printf("The length of the doubly linked list is %d\nReverse Data: ", tail->data);
    PDLNode p = tail->pre;
    while(p->pre){
        printf("%d ", p->data);
        p = p->pre;
    }
    printf("\n");
}

int main()
{
    PDLNode head;
    PDLNode tail;
    create_list(&head, &tail, 5);
    print_dlist_info(head);
    print_dlist_info_reverse(tail);
    insert(head, 3, 10);
    print_dlist_info(head);
    print_dlist_info_reverse(tail);
    return 0;
}

PS: 关于循环链表和静态双向链表我就不一一给出代码,因为他们只是很细微的变化。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值