线性表--顺序表示和实现(初始化、取值、查找、插入、删除)

        选用教材为“数据结构(C语言版 第三版)李冬梅 严蔚敏 吴伟民编著”

        线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,通常称这种存储结构的线性表为顺序表(Sequential List)。特点:地址连续、依次存放、随机存取、类型相同。

实验一设计如下:

  1. 回忆c/c++的基础知识,介绍clion软件的使用,联系上节课讲述的typedef 自定义抽象数据类型,完成练习;
  2. 第二节课讲述顺序表的基础操作;
  3. 带刷力扣题目。

一、c/c++的基础知识

指针

  • &和*为互补运算;
  • 指针必须指向对象后,才能引用。
  • #include <stdio.h>
    void main ()
    { 
      int x ,*p;
      x=55;
      p=&x;
      printf ( “ %d, %d\n”, x, *p) ;
      *p=65;
      printf ( “ %d, %d”, x, *p) ;
    }
    

        程序中 int *p; 只是定义了一个指针变量 p,此时它没有指向任何对象(内容是随机值)。
👉 如果直接使用 *p,会访问非法内存,程序可能报错。所以必须先执行 p = &x;,让 指向变量 x

&x:取变量 的地址

*p:取指针 所指向地址中的值。在程序中,p = &x;,所以 *p 就等价于 x。

比如执行 *p = 65; 其实就是 x = 65。之后 printf 输出 65, 65。
✔️ 这说明 *(&x) == x,&(*p) == p,互相抵消,是互补运算。

数组

  1. 数组的本质

    • 数组是一段连续的内存空间。

    • 数组名就是这段空间的首地址,它是一个常量

  2. 指针与数组的关联

    • 一个指针变量可以存储数组的首地址

    • int a[10];

    • int *p = a; (直接赋值,因为数组名 a 本身就是地址)

    • int *p = &a[0]; (取首个元素的地址,效果相同)

  3. 如何通过指针访问数组元素?

    • a[i] 这种下标访问是数组的标准用法。

    • 但它的底层原理是指针运算*(a + i)

    • a + i 表示从数组首地址 a 偏移 i 个元素单位的新地址

    • * (解引用) 运算符则用来获取这个新地址上的

  4. 总结

    • a[i] 和 *(a + i) 效果完全等价

    • 数组名 a 是常量地址,不能改变其值 (a = p 是错的)。

    • 指针 p 是变量地址,可以改变其指向 (p = a 是对的)。

c++中的参数传递

  •         传值方式
#include <iostream.h>
using namespace std; 

void swap(float m,float n)
{
    float temp;
    temp=m;
    m=n;
    n=temp;
}
void main()
{
    float a,b;
    cin>>a>>b;
    swap(a,b);
    cout<<a<<endl<<b<<endl;
}

转换失败:把实参的值传送给函数局部工作区相应的副本中,函数使用这个副本执行必要的功能。函数修改的是副本的值,实参的值不变。

  • 传地址方式-指针变量作参数(1)
  • #include <iostream>
    using namespace std;
    
    void swap(float *m,float *n)
    {
        float t;
        t=*m;
        *m=*n;
        *n=t;
    }
    
    void main()
    {
        float a,b,*p1,*p2;
        cin>>a>>b;
        p1=&a;
        p2=&b; 
        swap(p1, p2);
        cout<<a<<endl<<b<<endl;
    }
    

          这段代码成功的关键在于指针地址传递。当你调用 swap(p1, p2) 时,你没有传递 a 和 b 的值,而是把它们的内存地址传了进去。在 swap 函数内部,m 和 n 这两个指针就分别指向了 a 和 b 在内存中的位置。函数通过解引用操作符 *,直接找到了这些地址上的原始数据,并进行了交换。

  •      可以把这个过程想象成:你不是把两本书(值)的副本交给朋友去交换,而是直接把这两本书的位置(地址)告诉了他。这样,朋友就能直接在书架上把它们的位置换掉,而书架上的书(原始变量)也就真正被交换了。这就是指针实现地址传递的强大之处,它让函数可以直接操作函数外部的变量。

  • 传地址方式-指针变量作参数(2)
    #include <iostream>
    using namespace std;
    
    void swap(float *m,float *n)
    {
        float *t;
        t=m;
        m=n;
        n=t;
    }
    
    void main()
    {
        float a,b,*p1,*p2;
        cin>>a>>b;
        p1=&a;  
        p2=&b; 
        swap(p1, p2);
        cout<<a<<endl<<b<<endl;
    }
    

          上面是一个常见的指针陷阱。这段代码的目标是交换 a 和 b 的值,但它无法成功。问题出在 swap 函数内部。虽然我们传进去了两个指针 p1 和 p2,但函数在接收时,依然遵循值传递的原则。这意味着,swap 函数创建了两个新的局部指针变量 m 和 n,它们只是复制了 p1 和 p2 所存储的地址。

          函数内部的 t=m; m=n; n=t; 这三行代码,仅仅是在交换这些局部地址副本的值,让 m 指向 b,让 n 指向 a。一旦 swap 函数执行完毕,它内部的所有局部变量,包括 mn 和 t,都会被销毁。而 main 函数中的原始指针 p1 和 p2 及其所指向的 a 和 b,从头到尾都没有被触碰过。所以,a 和 b 的值当然也就纹丝不动。

          要让代码成功,我们必须解引用指针,直接操作它们所指向的内存空间。正确的做法是使用 * 符号,把 m 和 n 这两个指针当成“遥控器”,去改变 a 和 b 的值,而不是仅仅交换遥控器本身。

  •       记住,交换指针交换指针所指向的值是两码事。前者是无用功,后者才是实现功能的关键。

  • 传地址方式-引用类型作参数

    #include<iostream>
    using namespace std;
    
    void main(){
    	int i=5;
    	int &j=i;
    	i=7;
    	cout<<"i="<<i<<" j="<<j;
    }
    

          引用的作用可以一句话概括:它是变量的别名

  •       就像你给一个好朋友起了个外号,无论你叫他的大名还是外号,指的都是同一个人。在代码里,int &j=i; 就是给变量 i 起了个别名 j。从这一刻起,i 和 j 就是同一个东西,它们共享同一块内存地址。所以,当你修改 i 的值时,j 的值也同步改变。引用的这种特性,让它在函数参数传递中非常有用,可以让你直接操作函数外部的变量,而不需要像指针那样使用复杂的 * 和 & 符号。

  1. #include <iostream>
    using namespace std;
    
    void swap(float& m,float& n)
    {    
        float temp;
        temp=m;
        m=n;
        n=temp;
    }
    
    void main()
    {    
        float a,b;
        cin>>a>>b;
        swap(a,b);
        cout<<a<<endl<<b<<endl;
    }
    

    同学们,我们来看这段代码。它成功了吗?答案是肯定的,它完全正确!为什么呢?秘密就在 swap 函数的参数里,大家看,这里我们用的是 float& m 和 float& n。这个 & 符号在这里可不是取地址,它是 C++ 的一个神奇功能——引用

    引用是什么?

    引用,你可以把它理解为一个变量的“别名”或者“外号”。

    当我们在 main 函数里调用 swap(a, b) 时,我们不是把 a 和 b 的值复制一份传进去,而是给它们分别起了个别名。m 成了 a 的外号,n 成了 b 的外号。所以,在 swap 函数内部,我们对 m 和 n 的任何操作,比如赋值、交换,实际上都是直接作用在 main函数里的原始变量 a 和 b 身上。

    引用 vs 指针

    这和我们之前讲的指针有什么区别呢?

    用指针交换,我们得先取地址,再用 * 来解引用,语法上稍微复杂点。而引用就简单多了,你看,在 swap函数里,m 和 n 就像是普通的 float 变量,你可以直接操作它们,编译器会自动帮你搞定幕后的地址传递。

    总结一下:

  2. 指针是告诉你变量的“门牌号”,你得拿着门牌号去找变量。

  3. 引用更直接,它就是变量的“另一个名字”,你喊这个名字,变量就会响应。

  • 传地址方式-数组名作参数
    #include<iostream>
    #include<cstring>  // 添加这个头文件
    using namespace std;
    
    void sub(char b[]);  // 函数声明
    
    int main()
    {
        char a[10]="hello";
        sub(a);
        cout<<a<<endl;
        return 0;
    }
    
    void sub(char b[])  // 添加返回类型 void
    {
        strcpy(b,"world");
    }
    

    这段代码的核心是演示数组作为函数参数的传递方式

    当程序运行到 sub(a); 时,我们并没有把整个 a 数组复制一份传给 sub 函数。因为数组太大,复制会消耗大量内存和时间。在 C++ 中,数组名本身就代表着它的首地址。所以,sub 函数实际接收到的是 a 数组在内存中的位置信息

    在 sub 函数内部,char b[] 接收了这个地址。接着,strcpy(b, "world"); 这行代码就直接找到这个地址,并把 "world" 这个新字符串覆盖到原始数组 a 的内存空间里。

    所以,当 sub 函数执行完毕后,a 的内容已经从最初的 "hello" 变成了 "world"。这正是为什么 cout 最后会输出 "world" 的原因。

结构体

在 C 语言里,我们可以用结构体来组合不同类型的数据,例如一本书的书号、书名和价格。定义一个结构体类型,并用 Book 作为该结构体的新名字。结构体内部有三个成员:nonameprice,分别存储书号、书名和价格。定义结构体有两种常见方式:

使用 typedef:

typedef struct {
    char no[15];
    char name[50];
    float price;
} Book;

Book b[10];

不使用 typedef:

struct Book {
    char no[15];
    char name[50];
    float price;
};

struct Book b[10];  // 正确
    • struct Book 是结构体类型的名字。

    • 声明变量时必须写 struct Book,比如 struct Book b[10];

    • 每次使用这种结构体类型都需要加 struct,比较繁琐。

    • typedef 给结构体类型起了一个别名 Book

    • 声明变量时可以直接写 Book b[10];,不用写 struct,更加简洁方便。

    💡 总结:

    • struct Book 是结构体的原名,声明变量时要加 struct

    • typedef + struct 给结构体起了别名,可以直接用别名声明变量。

    • 换句话说,typedef 就像给结构体类型取了一个‘绰号’,使用时更方便。

    练习一

    简单练习题:输入并显示一本书的信息(指针+结构体)

    题目描述:

    定义一个结构体 Book,包含 书号、书名和价格

    用指针输入一本书的信息。

    用指针显示这本书的信息。

    #include <stdio.h>
    
    typedef struct {
        char no[15];
        char name[50];
        float price;
    } Book;
    
    int main() {
        Book b;          // 声明一个 Book 类型变量
        Book *p = &b;    // 声明一个指针,指向 b
    
        // 输入书的信息
        printf("请输入书号:");
        scanf("%s", p->no);       // 使用指针访问结构体成员
        printf("请输入书名:");
        scanf(" %[^\n]", p->name); // 读取带空格的字符串
        printf("请输入价格:");
        scanf("%f", &p->price);
    
        // 显示书的信息
        printf("\n书籍信息:\n");
        printf("书号:%s\n", p->no);
        printf("书名:%s\n", p->name);
        printf("价格:%.2f\n", p->price);
    
        return 0;
    }
    

    可能会出现的问题:获取输入price时,为什么用引用符号,而其他两个属性不用?

    在结构体中,不同类型的数据成员访问方式不同

    • 基本类型(如 intfloat 存的是具体的数值,所以在需要获取地址时要用 &p->price,因为 price 本身是一个数。

    • 数组类型(如 char name[50] 在表达式中会自动转换为指向数组首元素的指针,所以 p->name 本身就代表地址,不需要再加 &

    👉 总结成一句话:
    数值型成员要取地址需要 &,而字符数组名本身就是地址,不用加 &

    ### 关于顺序表基本操作的实现 #### 初始化顺序表 为了创建一个顺序表,通常会定义一个结构体来封装数据成员以及必要的方法。对于`SeqList`而言,其内部维护着一个用于存储元素的一维数组`data[]`记录当前列表长度的整型变量`length`。 ```c typedef struct { int data[MAXSIZE]; // 存储空间base, MAXSIZE为最大容量常量 int length; // 当前线性表中元素个数 } SeqList; ``` 初始化过程即设置新实例化对象的状态到初始状态,在此情况下就是把`length`设成零: ```c Status InitList(SeqList &L){ L.length = 0; return OK; } ``` [^1] #### 获取指定位置处的值 (GetElem) 获取某个特定索引上的元素非常简单,只需访问对应下标的数组项即可完成读取动作;在此之前要先验证所请求的位置是否有效(既不越界也不为空),如果一切正常就返回该位置上储存的数据值。 ```c Status GetElem(SeqList L,int i,ElemType &e){ if(i<1 || i>L.length) return ERROR; e=L.data[i-1]; return OK; } ``` #### 查找元素 (LocateElem) 当需要定位某具体数值首次出现的地方时,则遍历整个序列直到找到匹配为止,并记住第一次遇到目标值得地方作为结果反馈出去。这里假设比较条件由用户提供自定义函数指针cmp()决定相等与否。 ```c int LocateElem(SeqList L, ElemType e, Status (*compare)(ElemType, ElemType)){ for(int j=0;j<L.length;++j) if(compare(L.data[j],e)) return j+1; return 0; } ``` [^2] #### 插入新元素 (ListInsert) 向已有的线性表里加入新的成分意味着调整现有布局以腾出适当的空间容纳后来者。这涉及到两个方面的工作:一是确认插入点的有效性边界情况处理;二是移动后续项目让路给即将安置的新成员,之后更新计数器反映最新规模变化。 ```c Status ListInsert(SeqList &L, int i, ElemType e){ if(i<1||i>L.length+1) return ERROR; if(L.length>=MAXSIZE) return ERROR; for(int k=L.length;k>=i;--k) L.data[k]=L.data[k-1]; L.data[i-1]=e; ++L.length; return OK; } ``` #### 删除已有元素 (ListDelete) 从现有的集合里面去除选定条目同样需要注意安全检查环节,确保待删序号合理可行后再执行实际移除工作。一旦确定了合法的操作范围,就可以着手准备清理现场了——将紧跟在被选定点后的所有实体向前挪动一格填补空白区间的角色,最终记得减少总数量统计。 ```c Status ListDelete(SeqList &L, int i, ElemType &e){ if((i<1)||(i>L.length)) return ERROR; e=L.data[i-1]; for(int k=i;k<=L.length-1;++k) L.data[k-1]=L.data[k]; --L.length; return OK; } ```
    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值