05. 线性表

一、什么是线性表

  线性表是具有 相同 数据类型的 n(n≥0)个 数据元素 的 有限序列,其中 n 为 表长,当 n=0 时,线性表是一个空表。若用 L 命名线性表,则其一般表示为 \(L = (a_{1}, a_{2}, ..., a_{i}, a_{i+1}, ..., a_{n})\)

线性表的表示

  其中,\(a_{i}\) 是线性表中的 “第 i 个” 元素线性表中的 位序\(a_{1}\)表头元素\(a_{n}\)表尾元素。除了第一个元素外,每个元素有且仅有一个 直接前驱。除了最后一个元素外,每个元素有且仅有一个 直接后继

ADT List
{
Data:
    线性表L∈List, 整数i表示位置, 元素X∈ElementType;
Operation:
    List MakeEmpty(void);                       // 初始化一个空线性表L
    ElementType FindKth(List L, int i);         // 根据位序K,返回相应元素
    int FindElement(List L, ElementType X);     // 在线性表L中查找X的第一次出现位置
    void Insert(List L, int i, ElementType X);  // 在位序i前插入元素X
    void Delete(List L, int i);                 // 删除位序i的元素
    void Length(List L);                        // 返回线性表L的长度
} ADT List;

二、线性表的顺序存储实现

2.1、线性表的顺序存储

  利用数组的 连续存储空间顺序存放 线性表的各元素。逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

线性表的顺序存储

#define MAX_SIZE    10

typedef int ElementType;

typedef struct LNode
{
    ElementType Data[MAX_SIZE];
    int Last;
}LNode, * List;

  如果我们要访问下标为 i 的元素,可以通过以下方法:L.Data[i]PtrL->Data[i]。如果我们要获取线性表的长度,可以通过如下方法:L.Last+1PtrL->Last+1

  我们还可以通过动态分配的方式实现顺序表。

#define INIT_SIZE    10

typedef int ElementType;

typedef struct LNode
{
    ElementType * Data;
    int MaxSize;
    int Last;
}LNode, * List;

2.2、顺序表的主要操作

2.2.1、初始化

/**
 * @brief 创建空的顺序表
 * 
 * @return List 指向顺序表表头的指针
 */
List MakeEmpty(void)
{
    List PtrL = (List)malloc(sizeof(LNode));
    PtrL->Last = -1;
    return PtrL;
}

  如果我们使用分配的方式实现顺序表,则它的初始化方法如下:

/**
 * @brief 创建空的顺序表
 * 
 * @return List 指向顺序表表头的指针
 */
List MakeEmpty(void)
{
    List PtrL = (List)malloc(sizeof(LNode));
    PtrL->Data = (ElementType *)malloc(INIT_SIZE * sizeof(ElementType));
    PtrL->MaxSize = INIT_SIZE;
    PtrL->Last = -1;
    return PtrL;
}

  采用动态分配实现顺序表时,我们还可以增加动态数组的长度。

/**
 * @brief 增加动态数据的长度
 * 
 * @param PtrL 顺序表
 * @param Length 增加的长度
 */
void IncreaseSize(List PtrL, int Length)
{
    int i = 0;
    int * temp = PtrL->Data;
    PtrL->Data = (ElementType *)malloc((PtrL->MaxSize + Length) * sizeof(ElementType));

    for (i = 0; i <= PtrL->Last; i++)
    {
        PtrL->Data[i] = temp->Data[i];                                          // 将原表内容复制到新表
    }

    PtrL.MaxSize += Length;                                                     // 增加表空间
    free(temp);                                                                 // 释放原表空间
}

2.2.2、查找元素

/**
 * @brief 按位序查找
 * 
 * @param PtrL 顺序表
 * @param position 要查找的位置
 * @return ElementType 要查找的元素
 */
ElementType FindKth(List PtrL, int position)
{
    if (position < 0 || position > PtrL->Last + 1)
    {
        printf("查找位置有误!\n");
        return NULL;
    }
  

    return PtrL->Data[position - 1];
}

由于顺序表的各个元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第 i 个元素,即随机存取特性,时间复杂度为 \(O(1)\)

/**
 * @brief 查找元素X的位置
 * 
 * @param PtrL 顺序表
 * @param X 查找的元素
 * @return int 如果找到返回元素的索引,否则返回-1
 */
int FindElement(List PtrL, ElementType X)
{
    int i = 0;

    if (PtrL == NULL)
    {
        printf("链表为空!\n");
        return NULL;
    }

    while (i <= PtrL->Last && PtrL->Data[i] != X)
    {
        i++;
    }

    return (i <= PtrL->Last) ? i : -1;
}

查找成功的平均次数为 \(\frac{n + 1}{2}\),平均时间性能为 \(O(n)\)

最好情况:目标元素在表头,循环 1 次,最好时间复杂度为 \(O(1)\)

最坏情况:目标元素在表尾,循环 n 次,最坏时间复杂度为 \(O(n)\)

平均情况:假设目标元素出现在任何一个位置的概率相同,都是 \(p = \frac{1}{n}\)。目标元素在第 1 位,循环 1 次,目标元素在第 2 位,循环 2 次,……,目标元素在第 n 位,循环 n 次。\(平均循环次数 = \frac{1}{n} + 2 * \frac{1}{n} + 3 * \frac{1}{n} + …… + n * \frac{1}{n} = \frac{n * (n + 1)}{2} * \frac{1}{n} = \frac{n + 1}{n}\),平均时间复杂度为 \(O(n)\)

2.2.3、插入元素

顺序表的插入

/**
 * @brief 插入一个新元素
 * 
 * @param PtrL 顺序表
 * @param i 要插入的位置
 * @param X 要插入的元素
 */
void Insert(List PtrL, int i, ElementType X)
{
    int j = 0;

    if (PtrL->Last == MAX_SIZE - 1)                                             // 表空间已满,不能插入
    {
        printf("表已满\n");
        return;
    }
    if (i < 1 || i > PtrL->Last + 2)                                            // 插入位置不合法
    {
        printf("位置不合法\n");
        return;
    }
  
    for (j = PtrL->Last; j >= i - 1; j--)
    {
        PtrL->Data[j + 1] = PtrL->Data[j];                                      // 将a[i]~a[n]倒序向后移动
    }

    PtrL->Data[i - 1] = X;                                                      // 新元素插入
    PtrL->Last++;                                                               // 表长增1
}

最好情况:新元素插入到表尾,不需要移动元素,i=n+1,循环 0 次,最好时间复杂度为 \(O(1)\)

最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动,i=1,循环 n 次,最坏时间复杂度为 \(O(n)\)

平均情况:假设新元素插入到任何一个位置的概率相同,即 i=1,2,3,...,Last+2 的概率都是 \(p = \frac{1}{n + 1}\)。i=1,循环 n 次,i=2 时,循环 n-1 次,……,i=n+1时,循环 0 次。\(平均循环次数 = np + (n - 1) * p + (n -2) * p + …… + p = \frac{n * (n + 1)}{2} * \frac{1}{n + 1} = \frac{n}{2}\),平均时间复杂度为 \(O(n)\)

2.2.4、删除元素

顺序表的删除

/**
 * @brief 按位置删除表中元素
 * 
 * @param PtrL 顺序表
 * @param i 删除元素的位置
 */
void DeleteByPosition(List PtrL, int i)
{
    int j = 0;

    if (i < 1 || i > PtrL->Last + 1)                                            // 删除位置不合法
    {
        printf("不存在第%d个元素\n", i);
        return;
    }
  
    for (j = i; j <= PtrL->Last; j++)
    {
        PtrL->Data[j-1] = PtrL->Data[j];                                        // 将a[i+1]~a[n]倒序向前移动
    }
  
    PtrL->Last--;                                                               // 表长减1
}
/**
 * @brief 按元素删除表中元素
 * 
 * @param PtrL 顺序表
 * @param X 要删除的元素
 */
void DeleteByElement(List PtrL, ElementType X)
{
    int i = 0;
    for (i = 0; i < PtrL->Last + 1; i++)
    {
        if (PtrL->Data[i] == X)                                                 // 找到元素X所在的位置
        {
            break;
        }
    }
  
    if (i == PtrL->Last + 1)
    {
        printf("删除元素不存在!\n");
        return;
    }

    DeleteByPosition(PtrL, i + 1);
}

最好情况:删除表尾元素,不需要移动其它元素,i=n,循环 0 次,最好时间复杂度为 \(O(1)\)

最坏情况:删除表头元素,需要将后续的 n-1 个元素全都向前移动,i=1,循环 n 次,最坏时间复杂度为 \(O(n)\)

平均情况:假设删除任何一个元素的概率相同,即 i=1,2,3,...,Last+1 的概率都是 \(p = \frac{1}{n}\)。i=1,循环 n-1 次,i=2 时,循环 n-2 次,……,i=n时,循环 0 次。\(平均循环次数 = (n - 1) * p + (n -2) * p + …… + p = \frac{n * (n + 1)}{2} * \frac{1}{n} = \frac{n - 1}{2}\),平均时间复杂度为 \(O(n)\)

2.2.5、遍历顺序表

/**
 * @brief 遍历顺序表
 * 
 * @param PtrL 顺序表
 */
void PrintList(List PtrL)
{
    int i = 0;

    for (i = 0; i <= PtrL->Last; i++)
    {
        printf("%d ", PtrL->Data[i]);
    }
    printf("\n");
}

三、线性表的链式存储实现

3.1、线性表的链式存储

  我们可以使用链式存储来实现线性表,链式存储不要求逻辑上相邻的两个元素物理上也相邻。我们可以通过 “链” 建立数据元素之间的逻辑关系。插入和删除不需要移动数据元素,只需要修改 “链” 即可。

线性表的链式存储

  链表由一系列不必在内存上相连的结构组成。每个结构均含有表元素和指向包含该元素后继元的结构组成。我们称之为 Next 指针。最后一个单元的 Next 指针指向 NULL。

typedef int ElementType;

typedef struct LNode
{
    ElementType Data;
    struct LNode * Next;
} LNode, * List;

3.2、链表的主要操作

3.2.1、求表长

/**
 * @brief 求链表的表长
 * 
 * @param PtrL 链表
 * @return int 链表的长度
 */
int Length(List PtrL)
{
    List p = PtrL;                                                              // p指向表的第一个节点
    int j = 0;

    while (p != NULL)
    {
        p = p->Next;
        j++;                                                                    // 当前p指向的是第j个节点
    }
    return j;
}

3.2.2、查找元素

/**
 * @brief 按序号查找
 * 
 * @param PtrL 链表
 * @param K 要查找的序号
 * @return List 如果找到返回指向第K个的指针,否则返回NULL
 */
List FindKth(List PtrL, int K)
{
    List p = PtrL;                                                              // p指向表的第一个节点
    int i = 1;

    if (PtrL == NULL)
    {
        printf("链表为空!\n");
        return NULL;
    }

    if (i < 1 || i > Length(PtrL) + 1)
    {
        printf("查找的位置不合法\n");
        return PtrL;
    }

    while (p != NULL && i < K)
    {
        p = p->Next;
        i++;                                                                    // 当前p指向的是第j个节点
    }

    return (i == K) ? p : NULL;
}
/**
 * @brief 按值查找
 * 
 * @param PtrL 链表
 * @param X 要查找的元素
 * @return List 如果找到返回指向X的指针,否则返回NULL
 */
List FindElement(List PtrL, ElementType X)
{
    List p = PtrL;                                                              // p指向表的第一个节点

    while (p != NULL && p->Data != X)
    {
        p = p->Next;
    }

    return p;
}

3.2.3、插入元素

  1. 先构造一个新节点,用 s 执行。
  2. 再找到链表的第 i-1 个结点,用 p 指向。
  3. 然后修改指针,插入结点(p 之后插入新结点是 s)。

链表的插入

链表的表头插入

/**
 * @brief 插入一个元素
 * 
 * @param PtrL 链表
 * @param i 要插入的位置
 * @param X 要插入的元素
 * 
 * @return List 指向添加元素的链表的头指针
 */
List Insert(List PtrL, int i, ElementType X)
{
    List p = NULL, s = NULL;

    if (i < 1 || i > Length(PtrL) + 1)
    {
        printf("插入的位置不合法\n");
        return PtrL;
    }

    if (PtrL == NULL)
    {
        PtrL = (List)malloc(sizeof(LNode));
        PtrL->Data = X;
        PtrL->Next = NULL;
        return PtrL;
    }
  
    if (i == 1)                                                                 // 新节点插入在表头
    {
        s = (List)malloc(sizeof(LNode));                                        // 申请新节点
        s->Data = X;                                                            // 新节点的数据域为表头
        s->Next = PtrL;                                                         // 新节点指向原来的表头
        return s;
    }

    p = FindKth(PtrL, i - 1);                                                   // 找到第i-1个节点
    if (p == NULL)                                                              // 插入的位置不合法
    {
        printf("插入的位置不合法\n");
        return PtrL;
    }

    s = (List)malloc(sizeof(LNode));                                            // 申请新节点
    s->Data = X;
    s->Next = p->Next;                                                          // 新节点指向p的下一个节点
    p->Next = s;                                                                // p指向新节点

    return PtrL;
}

3.2.4、删除元素

  1. 先找到链表的第 i-1 个节点,用 p 指向。
  2. 再用指针 s 指向要被删除的节点(p 的下一个节点)。
  3. 然后修改指针,删除 s 所指节点。
  4. 最后释放 s 所指节点的空间。

链表的删除

链表的表头删除

/**
 * @brief 删除一个元素
 * 
 * @param PtrL 链表
 * @param i 要删除元素的位置
 * @return List 指向删除后的链表的头指针
 */
List DeleteByPositon(List PtrL, int i)
{
    List p = NULL, s = NULL;

    if (PtrL == NULL)
    {
        printf("链表为空!\n");
        return NULL;
    }

    if (i < 1 || i > Length(PtrL))
    {
        printf("删除的位置不合法\n");
        return PtrL;
    }

    if (i == 1)                                                                 // 要删除的节点是表头
    {
        s = PtrL;                                                               // s指向表头
        PtrL = PtrL->Next;                                                      // 删除表头,表头指向下一个节点
        free(s);                                                                // 释放s
        return PtrL;
    }

    p = FindKth(PtrL, i - 1);                                                   // 找到第i-1个节点
    if (p == NULL)
    {
        printf("第%d个节点不存在!\n", i - 1);
    }
    else if (p->Next == NULL)
    {
        printf("第%d个节点不存在!\n", i);
    }
    else
    {
        s = p->Next;                                                            // s指向第i个节点
        p->Next = s->Next;                                                      // p指向第i+1个节点
        free(s);                                                                // 释放s
    }

    return PtrL;
}
/**
 * @brief 按元素删除
 * 
 * @param PtrL 链表
 * @param X 要删除的元素
 * @return List 指向删除后的链表的头指针
 */
List DeleteByElement(List PtrL, ElementType X)
{
    List p = PtrL;
    LNode * node = FindElement(PtrL, X);
    LNode * next_node = NULL;

    if (p == NULL)
    {
        printf("链表为空!\n");
        return NULL;
    }

    if (node == NULL)
    {
        printf("要删除的元素不存在!\n");
        return PtrL;
    }

    next_node = node->Next;

    if (next_node != NULL)
    {
        node->Data = next_node->Data;
        node->Next = next_node->Next;
        free(next_node);
        return PtrL;
    }
    else
    {
        return DeleteByPositon(PtrL, Length(PtrL));
    }
}

3.2.5、遍历链表

/**
 * @brief 遍历链表
 * 
 * @param PtrL 链表
 */
void PrintList(List PtrL)
{
    List p = PtrL;
    while (p != NULL)
    {
        printf("%d ", p->Data);
        p = p->Next;
    }
    printf("\n");
}
posted @ 2023-06-23 21:14  星光樱梦  阅读(4)  评论(0编辑  收藏  举报