数据结构——线性表

本文详细介绍了线性表的顺序表示,包括静态分配和动态分配的顺序表,以及插入、删除、查找等基本操作。此外,还讨论了线性表的链式表示,如单链表、循环单链表、双链表和循环双链表,涵盖了它们的表示方法和各种操作。文章最后提到了静态链表的概念和基本操作思路。
摘要由CSDN通过智能技术生成

线性表的顺序表示

文章目录


🔸 随机访问,通过首地址和元素符号可以在 O(1)内找到指定的元素

🔸 存储密度高,每个节点只存储数据元素。

🔸 逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素。


静态分配(SqList)

表示方法

使用结构体表示静态分配的顺序表

结构体内容包括:

  1. 静态数组——存储数据元素,根据需要的 MaxSize 初始化,为其开辟存储空间。
  2. length——存储当前顺序表的长度
struct SqList {
   
    ElemType data[MaxSize];
    int length;				// 顺序表当前长度
};
typedef struct SqList {
   
    ElemType data[MaxSize];
    int length;				// 顺序表当前长度
}SqList;


基本操作

初始化静态分配的顺序表 InitList(SqList &L)

🔸 初始化时,必须将当前顺序表长度置为 0。

🔸 没必要给每个元素赋值。

void InitList(SqList &L){
   	// 引用调用 & ,带回改变的内容
    L.length = 0;			// 初始化时,必须将当前顺序表长度置为 0。
    // 其他初始化操作
}



动态分配(SeqList)

表示方法

使用结构体表示动态分配的顺序表

静态分配的顺序表使用静态数组初始化,当数据长度大于事先设置好的 MaxSize 后,只能自暴自弃了,而动态分配可以解决这一问题。

动态分配使用的依旧是数组的思想开辟一片新的更长的连续空间,再将之前的部分复制过来,最后释放先前的存储空间,基本操作中需要增加一个 IncreaseSize(SeqList &L, length) 来实现该操作。

结构体内容包括:

  1. 数据指针——指示动态分配数组的指针。它指向的是数组的首地址
  2. length——存储当前顺序表的长度。
  3. 顺序表的最大容量
typedef struct SeqList {
   
    ElemType *data;
    int MaxSize;		// 动态增加长度需要更改此值
    int length;			// 插入、删除元素需要更改此值
}SeqList;


基本操作

除特殊说明的操作外,以下操作静态动态均可使用。

初始化动态分配的顺序表 InitList(SeqList &L)

与静态顺序表不同,静态分配时在 struct 中通过数组初始化形式开辟了存储空间,而动态分配用指针指示数据,所以需要使用 malloc 或者 new 关键字来开辟存储空间。

InitList(SeqList &L){
   
    L.data = (ElemType *)malloc(sizeof(ElemType) * InitSize);
    // 在C++ 中也可以使用 new 关键字开辟存储空间。
    L.data = new ElemType[InitSize];
    
    L.length = 0;
    L.MaxSize = InitSize;
}


增加动态顺序表长度 IncreaseSize(SeqList &L, int len)

🔹 len 是扩展长度还是增加的长度需要具体问题具体看待。

🔸 步骤:

  1. 根据需要增加的长度 len 和当前长度 L.length,开辟一块新的连续存储空间,长度为 (len + length) * sizeof(ElemType)。
  2. 将先前 L 中存储的数据(长度为 L.length)全部复制到新开辟的空间上。
  3. L.MaxSize 最大存储数据长度更新。第二步和第三步可更改顺序。
  4. 释放先前的存储空间。
void IncreaseSize(SeqList &L, int len) {
   
    ElemType *p = L.data;							// 用 p 指针指向先前数据首地址
    L.data = (ElemType *)malloc( (len + L.length) * sizeof(ElemType) );	// step 1
    //或者使用 new 开辟空间: L.data = new ElemType[len + L.length]; 
    for (int i = 0; i < L.length; i++) {
   			// step 2
        L.data[i] = p[i];							// 或者采用指针操作方式: *(L.data + i) = *(p + i);
    }
    L.MaxSize = L.MaxSize + len;    				// step 3
    free(p);										// step 4
    // 或者使用 delete 释放空间: delete(p);										
    p = NULL;
}


插入:按位序插入数据元素 ListInsert(SeqList &L, int i, ElemType e)

在顺序表某个“位序”插入数据元素,应当将该位序上及该位序以后的元素全部后移 1 位,然后给该位序元素赋值。

🔸 为保证程序鲁棒性,需要考虑:

  1. 指定位序是否在有效范围(1~L.length即在当前长度范围内)内。
  2. 顺序表已满,无法继续插入元素时,继续插入元素会报错。

🔸 步骤:

  1. 判断指定的位序是否在有效范围。
  2. 判断顺序表是否已满。
  3. 将该位序上及该位序以后的元素全部后移 1 位,然后给该位序元素赋值(下标为 i - 1)。
  4. L.length ++;

🔸 从某个下标开始,全部元素后移一位,需要从末尾开始循环,将前面一个元素赋值给它的后继元素,L.length 作为下标恰好是末尾元素的后继元素,i 即位序,i 作为下标是位序元素的后继元素。

  • 站在被赋值的后面元素角度上看:从末尾元素的后继元素(下标为 L.length )到 位序元素的后继元素(下标为 i)均需要其前驱元素给其赋值,所以循环下标需要从 L.length 至 i。
  • 站在前面赋值元素的角度上看:同理,从末尾元素(下标为 L.length - 1)到“位序元素”(下标为 i - 1)都需要给后继元素赋值。
bool ListInsert(SeqList &L, int i, ElemType e) {
   
    if(i < 1 || i > L.length)
        return flase;
    if(L.Length == L.MaxSize)
        return flase;
    for(int index = L.length; index >= i; index --) {
   
        data[index] = data[index - 1];
    }
    data[i - 1] = e;
    L.length ++;
    return true;
}


删除:按位序删除数据元素,并用 e 返回 ListDelete(SeqList &L, int i, ElemType e)

🔹 i 位序,e 被删除的元素——靠指针或者引用传递来“带回”。

与插入同理,删除只需将“位序”后的元素全部前移 1 位即可。

🔸 为保证程序鲁棒性,需要考虑:

  1. 指定位序是否在有效范围(1~L.length即在当前长度范围内)内。
  2. 顺序表为空,无法删除数据。

🔸 步骤:

  1. 判断鲁棒性
  2. “位序”后元素前移 1 位。
  3. L.length–;

【注】前移 1 位,操作起来应该从前往后。i 为下标对应“位序”元素的后继。(两种写法)

  • 站在前面被赋值元素的角度看:从“位序元素”(下标为 i - 1)开始,至末尾元素的前驱(下标为 L.length - 2),每次循环中元素的后继赋值给它。
  • 站在后面给前面赋值的元素角度看:从“位序元素”的后继元素(下标是 i )到末尾元素(下标 L.length - 1),每次循环中给前驱元素赋值。
bool ListDelete(SeqList &L, int i, Elemtyep &e) {
   
    if(i < 1 || i > L.length)
        return flase;
    if(L.length == 0)
        return flase;
    e = data[i - 1];
    for (int index = i - 1; index <= L.length - 2; index ++) {
   
        data[index] = data[index + 1];
    }
    // 或
    for (int index = i; index <= L.length -1; index ++) {
   
        data[index - 1] = data[index];
    }
    L.length--;
}


查找:按“位序”查找元素并返回 GetElem(SeqList L, int i)

🔸 返回类型是 ElemType,i 是位序,对应下标是 i - 1。

ElemType GetElem(SeqList L, int i) {
   
    return L.data[i - 1];
}


查找:按值查找,返回其位序 LocateElem(SeqList L, ElemType e)

🔸 注意返回的是位序还是下标

int LocateElem(SeqList L, ElemType e) {
   
    for (int i = 0; i < L.length; i++) {
   
        if (L.data[i] == e)
            return i + 1;		// 位序 = index + 1
    }
    return 0;					// 没找到与 e 相等的元素,即返回 0
}




线性表的链式表示

🔸 对每个链表结点,除存放元素自身的信息外,还需要存放指向其前驱、后继的指针。

🔸 增 删结点方便。

🔸 查找结点需遍历,麻烦。


单链表(LinkList)

表示方法

使用多个内部带有指向后继结点指针的结构体构成一个单链表

单链表的数据元素由结构体构成,其组成:

// c 写法:
typedef struct LNode {
   
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

// cpp 写法:
typedef struct LNode {
   
    ElemType data;
    LNode *next;
}*LinkList;					// 无需 typedef LNode 了,因为 C++ 中 struct 类名即可当做类型来声明。

❗ 注意,typedef struct LNode {...;} *LinkList; 将结构体类型和结构体指针类型分别命名为 LNode 和 LinkList,这就意味着:struct LNode *p 等价于 LinkList p

这么做并非无理取闹、哗众取宠,而是旨在让代码更具有可读性——LNode * elem 代表指向每个结点的指针,而 LinkList L 则代表指向单链表首地址的指针。

🔸 因此不要忘记,LinkList 声明的是一个 struct LNode 类型的指针,因此在取结构体内容时务必使用 ->

那么我们就有了单链表的一些规范:

为结点开辟存储空间,我们用 LNode 配合 malloc 或者 new。

LNode * node = (LNode *)malloc(sizeof(LNode));
LNode * node 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值