我的数据结构与算法「一般线性表」

一般线性表

一般线性表简称线性表,是线性逻辑结构且具有相同数据类型的 n n n 个数据元素的有限序列。其中 n n n 为表长。当 n = 0 n=0 n=0 时 线性表是一个空表;线性表中第一个数据元素称为表头数据元素有且只有一个直接后继,最后一个数据元素称为表尾数据元素有且只有一个直接前驱,其他数据元素有且仅有一个直接前驱与直接后继,存储结构分为顺序存储与链式存储分别对应一般线性表中的顺序表于链表

image-20220328161128357

线性表的主要操作

  • Init() 初始化表操作,构造一个空的线性表
  • Length(L) 求表长操作,返回线性表工的长度,即表 L L L 中数据元素的个数
  • Print(L) 输出操作,按前后顺序输出线性表工 的所有元素值
  • Insert(L,i,e) 插入操作,在表 L L L 中的第i个位置上插入指定元素 e e e
  • Delete(L,i) 删除操作,删除表 L L L 中第 i i i 个位置的元素
  • LocateElement(L,e) 按值查找操作,在表 L L L 中查找具有给定元素的值的位序
  • GetElement(L,i) 按位查找操作,获取表 L L L 中第 i i i 个位置的元素的值
  • Empty(L) 判空操作,若 L L L 为空表,则返回 t r u e true true,否则返回 f a l s e false false
  • Destroy(L) 销毁操作,销毁线性表,并释放线性表 L L L 所占用的内存空间

C C C 语言中,存在一个称之为栈的自动存储区, i n t    a r r a y [ 10 ] int\;array[10] intarray[10] 内存空间由编译器自动分配释放;存在一个称之为的堆的自由存储区,用 m a l l o c ( ) malloc() malloc() f r e e ( ) free() free() 函数手动完成动态内存管理而非系统自动,利用 m a l l o c ( ) malloc() malloc() 为定义的数据结构分配一块实际所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为基地址;若分配失败,则返回 N U L L NULL NULL,已分配的空间可用 f r e e ( ) free() free() 释放

顺序表

顺序存储的线性表称为顺序表( C C C 语言中的数组)它用一组地址连续的存储空间依次存储数据元素,从而使得逻辑上相邻的两个数据元素在物理位置上也相邻,顺序表至少包含三个属性

  • 存储空间的起始位置

  • 顺序表最大存储空间

    定义好允许的最大长度,也可以动态分配存储空间,在程序执行过程中通过动态存储分配语句分配

  • 顺序表当前的长度

image-20220328195657245

顺序表最主要的特点是随机访问,通过首地址和数据元素序号可以在 O ( 1 ) O(1) O(1) 的时间内找到指定的数据元素,其次顺序表的存储密度高,每个结点只存储数据元素。顺序表逻辑上相邻的元素物理上也相邻,无需消耗额外存储空间建立顺序表中数据元素间的逻辑关系,但是插入、删除和查找等操作需要移动大量元素

顺序表的实例
#include <stdio.h>
#include <stdlib.h>
#define Size 5

// 定义布尔类型false=0与true=1
typedef enum
{
    false,
    true
} bool;

// 定义结构类型SequenceTabel[...]叫ST
typedef struct SequenceTabel
{
    int *head;
    int size;
    int length;
} ST;

/* Init */
ST initST();
/* Length */
int lengthST(ST st);
/* Print */
void printST(ST st);
/* Insert */
ST insertST(ST st, int idx, int elem);
/* Delete */
ST deleteST(ST st, int idx);
/* LocateElement */
bool LocateelementST(ST st, int elem);
/* GetElement */
int getelementST(ST st, int idx);
/* Empty */
bool emptyST(ST st);
/* Destroy */
bool destroyST(ST st);

ST initST()
{
    ST st;
    /* malloc申请一块连续的指定大小的内存块区域作为数组
    分配成功则返回数组的头指针,否则返回空指针NULL
    calloc(size,sizeof(int))会初始化数组元素为0
    sizeof计算一个int型变量占内存多少单元
    (int *)将指针指向的数据强制转换为整型 */
    st.head = (int *)malloc(Size * sizeof(int));
    if (!st.head)
    {
        printf("没有分配内存");
        exit(0);
    }
    st.size = Size;
    st.length = 0;
    return st;
}

int main()
{
    // 初始化顺序素
    ST st = initST();
    for (int i = 1; i <= Size; i++)
    {
        st.head[i - 1] = i;
        st.length++;
    }

    // 求表长
    int l;
    l = lengthST(st);
    printf("顺序表长%d\n", l);

    // 打印元素
    printf("打印初始化的顺序表\n");
    printST(st);

    // 插入元素
    st = insertST(st, 1, 6);
    printf("打印插入元素后的顺序表\n");
    printST(st);

    // 删除元素
    st = deleteST(st, 1);
    printf("打印删除元素后的顺序表\n");
    printST(st);

    // 按值找位
    int e = 9;
    int idx;
    idx = locateelementST(st, e);
    if (idx)
    {
        printf("查询元素%d在的顺序表的序位是%d\n", e, idx);
    }
    else
    {
        printf("查询元素%d不在的顺序表\n", e);
    }

    // 按位找值
    e = getelementST(st, idx = 4);
    printf("顺序表位序%d的元素是%d\n", idx, e);

    // 判空
    bool flag;
    flag = emptyST(st);
    if (flag)
    {
        printf("顺序表是空的\n");
    }
    else
    {
        printf("顺序表不是空的\n");
    }

    // 销毁
    destroyST(st);

    return 0;
}

int lengthST(ST st)
{
    int e = st.length;
    return e;
}

void printST(ST st)
{
    for (int i = 0; i < st.length; i++)
    {
        int print = st.head[i];
        printf("打印第%d的值是:%d\n", i, print);
    }
    printf("\n");
}

ST insertST(ST st, int idx, int elem)
{
    if ((idx < 0) || (idx > st.length + 1))
    {
        printf("插入位置有问题\n");
        return st;
    }
    if (st.length == st.size)
    {
        st.head = (int *)realloc(st.head, (st.size + 1) * sizeof(int));
        if (!st.head)
        {
            printf("没有头指针存储空间分配失败\n");
            return st;
        }
        st.size += 1;
    }
    for (int i = st.length - 1; i >= idx - 1; i--)
    {
        st.head[i + 1] = st.head[i];
    }
    st.head[idx - 1] = elem;
    printf("出入元素%d\n", elem);
    st.length++;
    return st;
}

ST deleteST(ST st, int idx)
{
    if ((idx < 0) || (idx > st.length))
    {
        printf("删除位置有问题\n");
        return st;
    }
    int elem = st.head[idx - 1];
    printf("删除元素%d\n", elem);
    for (int j = idx; j < st.length; j++)
    {
        st.head[j - 1] = st.head[j];
    }
    st.length -= 1;
    st.head = (int *)realloc(st.head, (st.size - 1) * sizeof(int));
    st.size -= 1;
    return st;
}

bool locateelementST(ST st, int elem)
{
    for (int i = 0; i < st.length; i++)
    {
        if (st.head[i] == elem)
        {
            return i + 1;
        }
    }
    return false;
}

int getelementST(ST st, int idx)
{
    int e = st.head[idx];
    return e;
}

bool emptyST(ST st)
{
    if (st.length == 0)
    {
        return true;
    }
    return false;
}

bool destroyST(ST st)
{
    free(st.head);
    st.head = NULL;
    return true;
}
顺序表的插入

算法思路

  • 判断插入结点位置是否满足顺序表定义
  • 判断存储空间是否已达上限,是则扩充存储空间
  • 将插入结点位置到表尾的结点分别向后移动一位
  • 腾出的存储空间插入数据元素并调整表长属性
// 找到插入位置,插入位置之后的元素往后移,给插入元素腾地方
ST addST(ST st, int idx, int elem)
{
    // 边界条件1[插入的位置不能是最后一个位置的后两个位置]
    if ((idx < 0) || (idx > st.length + 1))
    {
        printf("插入位置有问题\n");
        return st;
    }
    // 边界条件2[如果t.length==t.size意味着,内存空间已经满了,需要开辟新的空间]
    if (st.length == st.size)
    {
        // 这里函数realloc重新分配存储空间,参数是需要扩充的头指针与扩充之后的总的大小
        st.head = (int *)realloc(st.head, (st.size + 1) * sizeof(int));
        if (!st.head)
        {
            printf("没有头指针存储空间分配失败\n");
            return st;
        }
        st.size += 1;
    }
    // 后挪腾出存储空间并插入
    for (int i = st.length - 1; i >= idx - 1; i--)
    {
        st.head[i + 1] = st.head[i];
    }
    st.head[idx - 1] = elem;
    printf("出入元素%d\n", elem);
    st.length++;
    return st;
}

时间复杂度

  • 「最好情况」在表尾插入结点,结点后移语句将执行 0 0 0 次,时间复杂度为 O ( 1 ) O(1) O(1)

  • 「最坏情况」在表头插入结点,结点后移语句将执行 n n n 次,时间复杂度为 O ( n ) O(n) O(n)

  • 「平均情况」在长度为 n n n 的线性表中插入一个结点所需移动结点的平均次数为 ∑ i = 1 n + 1 1 n + 1 ( n − i + 1 ) = n 2 \sum_{i=1}^{n+1}\frac{1}{n+1}(n-i+1)=\frac{n}{2} i=1n+1n+11(ni+1)=2n,时间复杂度为 O ( n ) O(n) O(n)

顺序表的删除

算法思路

  • 判断删除结点位置是否满足顺序表定义
  • 获取删除结点的数据元素
  • 将被删结点后面的所有元素都依次向前移动一个结点并调整表长属性
// 找到删除位置,把其后的元素往后前
ST deleteST(ST st, int idx)
{
    // 边界条件1[删除的位置不能超出表的范围]
    if ((idx < 0) || (idx > st.length))
    {
        printf("删除位置有问题\n");
        return st;
    }
    // 取出删除元素,并把其后的元素前移
    int elem = st.head[idx - 1];
    printf("删除元素%d\n", elem);
    for (int j = idx; j < st.length; j++)
    {
        st.head[j - 1] = st.head[j];
    }
  	// 重新表长属性
    st.length -= 1;
  	/* 如需调整存储空间则执行
       st.head = (int *)realloc(st.head, (st.length) * sizeof(int));
       st.size -= 1; */
    return st;
}

时间复杂度

  • 「最好情况」删除表尾结点,无须移动结点,时间复杂度为 O ( 1 ) O(1) O(1)
  • 「最坏情况」删除表头结点,需要移动除第一个元素外的所有结点,时间复杂度为 O ( n ) O(n) O(n)
  • 「平均情况」在长度为 n n n 的线性表中删除一个结点所需移动结点的平均次数为 ∑ i = 1 n 1 n ( n − i + 1 ) = n − 1 2 \sum_{i=1}^{n}\frac{1}{n}(n-i+1)=\frac{n-1}{2} i=1nn1(ni+1)=2n1,时间复杂度为 O ( n ) O(n) O(n)

链表

链表是链式存储结构的线性表

单链表

线性表的链式存储又称单链表,通过一组任意的存储单元来存储线性表中的数据元素。每个链表结点除存放数据元素自身外,还存放一个指向其后继的指针,建立数据元素之间的线性关系

image-20220329154732443
/* 定义单链表结点类型,结点是一个结构类型struct ListNode,
	 有两个数据项,一个是整型data,另一个指针next
	 其中的指针是指向同样的结构类型的数据元素
	 这种类型是又包括两个成员的指针,嵌套定义即自己来定义自己 
	 式typrdef struct ListNode [...] LNode, *Linklist;是 
	 将[...]规则定义的结构体ListNode取名LNode
	 typedef struct ListNode [...] LNode;
	 将[...]规则定义的结构体ListNode的指针取名LinkList用于定义指针
   typedef struct ListNode [...] *LinkList; */
typedef struct ListNode
{
  // 数据域
  int data;
  // 指针域
  struct ListNode *next;
}LNode, *LinkList;

单链表解决了顺序表需要大量地址连续的存储空间的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。由于单链表的数据元素离散地分布在存储空间中,所以单链表是非随机存取的存储结构,即不能直接找到表中某个特定的结点。查找某个特定的结点时,需要从表头开始遍历,依次查找

  • 用头指针来标识一个单链表

    单链表 L L L,头指针为 N U L L NULL NULL 时表示一个空表。为了操作上的方便可在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向单链表的第一个结点,头指针始终指向链表的第一个结点(头结点是带头结点链表中的第一个结点)

    image-20220329105204542
  • 引入头结点可以带来两个优点

    使得在第一元素结点前插入结点与删除第一结点等操作与其它结点的操作统一

    无论链表是否为空,其头指针是指向头结点的非空指针,空表和非空表的操作统一

头插法建立单链表

建立新的结点分配内存空间,将新结点插入到当前单链表作为第一个结点,时间复杂度为 O ( n ) O(n) O(n)

截屏2022-03-29 10.59.10
// 逆向建立单链表
LinkList List HeadInsert(IinkList &L)
{
    LNode *S;  // 等价于struct ListNode *S
    int x;
    // 创建头结点其数据元素是指针而该指针指向的是定义的结构类型的数据元素
    L = (LinkList)malloc(sizeof(LNode));
    // 初始为空链表
    L->next = NULL;
    // 输入结点的值
    scanf("%d", &x);
    while (x != 9999)
    {
        // 创建新结点
        S = (LNode *)mal1oc(sizeof(LNode));
        S->data = x;
        S->next = L->next;
        // 将新结点插入表中L为头指针
        L->next = S;
        scanf("%d", &x);
    }
    return L;
}
尾插法建立单链表

建立新的结点并分配内存空间,将新结点插入到当前链表的表尾,使得读入数据的顺序和单链表中结点的顺序一致,因此需要增加一个尾指针 r r r 使其始终指向当前单链表的尾结点,时间复杂度为 O ( n ) O(n) O(n)

截屏2022-03-29 11.02.06
// 正向建立单链表
LinkList List TailInsert(IinkList &I)
{
  	// r为表尾指针
    LNode *S, *r = L;
	  // 输入结点的值
    scanf("%d", &x);  
    while (x != 9999)
    {
        S = (LNode *)malloc(sizeof(LNode));
        S->data = x;
        r->next = S;
      	// 指向新的表尾结点
        r = S;
        scanf("%d", &x);
    }
  	// 尾结点指针置空
    r->next = NULL;
    return L;   
}
按序号查找结点

从单链表的第一个结点开始,顺指针域 n e x t next next 逐个往下搜索,直到找到第 i i i 个结点为止,否则返回最后一个结点指针域 N U L L NULL NULL,时间复杂度为 O ( n ) O(n) O(n)

LNode *GetElem(LinkList L, int i)
{
    int j = 1;
    LNode *p = L->next;
    if (i == 0)
        return L;
    if (i < 1)
        return NULL;
    while (p && j < i)
    {
        p = p->next;
        j++;
    }
    return p;
}
按值查找结点

从单链表第一个结点开始,由前往后依次比较表中各结点数据域的数据元素,若某结点数据域的数据元素等于给定的 e e e,则返回该结点的指针,若整个单链表中没有这样的结点,则返回 N U L L NULL NULL,时间复杂度为 O ( n ) O(n) O(n)

LNode *LocateElem(LinkList L, int e)
{
    LNode *p = L->next;
    while (p !=NUIL && p->data != e)
            p = p->next;
    return p;
}
插入操作

将值为 x x x 的新结点 ∗ s *s s 插入到单链表的第 i i i 个位置上。首先检查插入位置的合法性,然后找到待插入位置的前驱结点 ∗ p *p p,再在第 i − 1 i−1 i1 个结点后插入新结点,时间复杂度为 O ( n ) O(n) O(n)

截屏2022-03-29 11.02.06
  • 查找指向插入位置的前驱结点的指针

    p=GetElem(L,i-1);

  • 令新结点 ∗ s *s s 的指针域指向 p p p 的后继结点

    s->next=p->next;

  • 令结点 p p p 的指针域指向新插入的结点 ∗ s *s s

    p->next=s;

  • 一种特殊方式是直接在将新结点插入到单链表的第 i i i 个结点后作为第 i + 1 i+1 i+1 个结点,然后直接交换两个结点的数据,节省了查找第 i i i 个结点的前驱第 i − 1 i-1 i1 个结点的时间,时间复杂度为 O ( 1 ) O(1) O(1)
删除操作

将单链表的第 i i i 个结点 ∗ q *q q 删除。首先检查删除位置的合法性,然后查找表中第 i − 1 i−1 i1 个结点即被删结点的前驱结点 ∗ p *p p ,修改 a a a 指向 c c c 后再将结点 ∗ q *q q 删除,时间复杂度为 O ( n ) O(n) O(n)

image-20220329155702031
  • 查找删除位置的前驱结点的指针

    p=GetElem(L,i-1);

  • 取指向删除位置的指针

    q=p->next;

  • p p p 指向被删除结点的后继

    p->next=q->next

  • 释放删除结点

    free(q);

  • 一种特殊方式是直接交换单链表的第 i i i 个结点与后继结点的数据,然后令第 i i i 个结点指向后继结点的后继结点再直接删除第 i i i 个结点的后继结点,节省了查找第 i i i 个结点的前驱第 i − 1 i-1 i1 个结点的时间,时间复杂度为 O ( 1 ) O(1) O(1)

求表长操作

计算单链表中数据结点 (不含头结点)的个数,需要从第一个结点开始顺序依次访问表中的每个结点,为此需要设置一个计数器变量,每访问一个结点,计数器加 1 1 1,直到访问到空结点为止,算法的时间复杂度为 O ( n ) O(n) O(n)

双链表

单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。插入与删除操作时需要访问某个结点的前驱结点,就只能从头开始遍历,访问后继结点的时间复杂度为 O ( 1 ) O(1) O(1) 而访问前驱结点的时间复杂度则为 O ( n ) O(n) O(n)

为了克服单链表的上述缺点引入了双链表,双链表结点中有两个指针 p r i o r prior prior n e x t next next,分别指向其前驱结点和后继结点

image-20220329154939708

建立双链表同样有头插法与尾插法两种

插入操作

在双链表中 p p p 所指的结点之后插入值为 x x x 的新结点 ∗ s *s s

image-20220329163401681
  • 1. s->next=p->next;
  • 2. p->next->prior=s;
  • 3. s->prior=p;
  • 4. p->next=s;
删除操作

删除双链表中 ∗ p *p p 的后继结点 ∗ q *q q f r e e ( q ) free(q) free(q)

image-20220329164428796
  • 1. p->next=q->next;
  • 2. q->next->prior=p;
循环链表与静态链表

循环单链表和单链表的区别在于,表中最后一个结点的指针不是 N U L L NULL NULL,而是指向头结点,从而整个链表形成一个环

image-20220329164940206

循环双链表类比循环单链表,循环双链表链表区别于双链表就是首尾结点构成环,当循环双链表为空表时,其头结点的指针域 p r i o r prior prior 和指针域 n e x t next next 都等于头结点的指针

image-20220329165103810

静态链表是用数组来描述线性表的链式存储结构,数组第一个元素不存储数据,它的指针域存储第一个元素所在的数组下标,静态链表最后一个元素的指针域值为 − 1 -1 1

image-20220329165430282
  • 从数组的第一个数据元素开始 a a a 指向 b b b 指向 c c c 指向 d d d 结束
  • 静态链表是通过顺序存储结构来模拟链式存储结构的特殊情形,通常用于不支持指针的高级语言,从这点不难发现链表相对顺序表更难实现,因为任何高级语言中都有数组类型而链表只存在于支持指针的高级语言中
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昊大侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值