408线性表【跨考小白学习笔记1】单链表中指针的使用(带头结点)

本文介绍了C语言中单链表的定义、初始化、头尾插入、按值查找、按位序插入与删除等基本操作。详细解析了指针的使用,包括头插法、尾插法创建链表,以及查找、插入和删除结点的过程。同时,文章讨论了指针解引用和传递指针参数时的注意事项。
摘要由CSDN通过智能技术生成

408线性表【跨考小白学习笔记1】——单链表定义与基本操作 (包含指针的使用)

直接看指针部分:
点击跳到指针部分


线性表的链式表示-单链表

1. 定义数据类型(头结点)

typedef struct LNode {  //定义单链表结点类型
  Elemtype data;        //每个结点存放一个数据元素
  struct LNode* next;   // 指针指向下一个结点
} LNode, *LinkList;     // Linklist,单链表
//LinkList是用typedef定义的自定义链表类型,凡是用该类型定义的变量都是指针,不需要加*号定义,而用Linklist head与TYPE *head其实都是指链表的头指针,没有多大的区别

LinkList是用typedef定义的自定义链表类型,凡是用该类型定义的变量都是指针,不需要加*号定义,而用Linklist head与TYPE *head其实都是指链表的头指针,没有多大的区别
点击跳回指针


2. 单链表的定义

2.1 初始化

王道的代码(c++):在这里插入图片描述

bool InitList(LinkList &L) {
  L = (LNode*)malloc(sizeof(LNode));  //分配一个头结点
  if (L == NULL)
    return false;  //分配失败
  L->next = NULL;  //头结点后暂时没有结点
  return true;
}

修改后的c语言代码:

//初始化一个单链表(带头结点)
bool InitList(LinkList* L) {
  *L = (LNode*)malloc(sizeof(LNode));  //分配一个头结点
  if (*L == NULL)
    return false;     //分配失败
  (*L)->next = NULL;  //头结点后暂时没有结点
  return true;
}

点击跳转:初始化中的指针使用


3. 单链表的基本操作

3.1 建立

3.1.1 头插法

逆向,每次把新的插在表头
王道的代码是这样的:(只建立,所以会new一个新的头结点给到原来的头指针)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ivdFKlD-1630835190821)(res/2021-08-06-19-29-24.png)]

更改后的c语言代码

//头插法建立单链表(逆向,每次都把新的插在最前面)
LinkList List_headInsert(LinkList* L) {
  LNode* s;
  Elemtype x;
  //  *L = (LinkList )malloc(sizeof(LNode));  //创建头结点
  //  (*L)->next = NULL; //error:Thread 1: EXC_BAD_ACCESS (code=1, address=0x8)
  //  由于初始化将头结点的next已赋为null,解引用访问了已被释放的首元节点,出现野指针错误
  //当某个对象被完全释放,再去通过该对象去调用其它的方法就会出现野指针错误。
  printf("请输入元素值,按9999退出输入");
  scanf("%d", &x);
  while (x != 9999) {
    s = (LNode*)malloc(sizeof(LNode));  //创建新结点
    s->data = x;
    s->next = (*L)->next;
    (*L)->next = s;
    printf("请输入元素值插入表头,按9999退出输入");
    scanf("%d", &x);
  }
  return *L;  //返回头结点
} 


3.1.2 尾插法

正向,每次把新的插在末尾
王道的代码是这样的:(只建立,所以会new一个新的头结点给到原来的头指针)

//尾插法建立单链表(正向,每次把新的插在末尾)
LinkList List_TailInsert(LinkList &L) {
  int x;
    L = (LinkList)malloc(sizeof(LNode));
    LNode *s,*r=L;
    printf("请输入元素值插入表尾,按9999退出输入");
  scanf("%d", &x);
  while (x != 9999) {
    s = (LNode*)malloc(sizeof(LNode));  //创建新结点
    s->data = x;
    r->next = s;
    r = s;  // r指向新的表尾结点
      printf("请输入元素值插入表尾,按9999退出输入");
      scanf("%d", &x);
  }
  r->next = NULL;  //尾结点指针置空
  return L;
}

完成更改后的c语言程序:(包括对已存在的表进行尾插)
#define Elemtype int //这里不用加分号

 //尾插法建立单链表(正向,每次把新的插在末尾)
LinkList List_TailInsert(LinkList* L) {
  Elemtype x;
  //  *L = (LinkList)malloc(sizeof(LNode));//这句要注释掉因为之前使用的L初始化后再次分配一个头结点将会使之前建立的结点都抛弃掉,重新弄一个新表
    //如果是需要new一个新表可以不注释
  
  LNode *s, *r = GetElem(*L, ListLength(*L));//遍历两遍,但还是O(N)
  
  printf("请输入元素值插入表尾,按9999退出输入");
  scanf("%d", &x);
  while (x != 9999) {
    s = (LNode*)malloc(sizeof(LNode));  //创建新结点
    s->data = x;
    r->next = s;
    r = s;  // r指向新的表尾结点
    printf("请输入元素值插入表尾,按9999退出输入");
    scanf("%d", &x);
  }
  r->next = NULL;  //尾结点指针置空
  return *L;
}
3.1.3 随机数头插法创建(源码出自大话数据结构)
/*  随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
void CreateListHead(LinkList* L, int n) {
  LinkList p;
  int i;
  srand(time(0)); /* 初始化随机数种子 */
                  //    *L = (LinkList)malloc(sizeof(LNode));
  //    (*L)->next = NULL;                      /*  先建立一个带头结点的单链表
  //    */
  for (i = 0; i < n; i++) {
    p = (LinkList)malloc(sizeof(LNode)); /*  生成新结点 */
    p->data = rand() % 100 + 1;          /*  随机生成100以内的数字 */
    p->next = (*L)->next;
    (*L)->next = p; /*  插入到表头 */
  }
}
3.1.4 随机数尾插法创建(源码出自大话数据结构)
  /*  随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
  void CreateListTail(LinkList * L, int n) {
    LinkList p, r;
    int i;
    srand(time(0));                      /* 初始化随机数种子 */
    *L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
    r = *L;                              /* r为指向尾部的结点 */
    for (i = 0; i < n; i++) {
      p = (Node*)malloc(sizeof(Node)); /*  生成新结点 */
      p->data = rand() % 100 + 1;      /*  随机生成100以内的数字 */
      r->next = p; /* 将表尾终端结点的指针指向新结点 */
      r = p;       /* 将当前的新结点定义为表尾终端结点 */
    }
    r->next = NULL; /* 表示当前链表结束 */
  }

3.2 查找或取值

3.2.1 按序号查找结点值

最好时间复杂度为 O ( 1 ) O(1) O(1):即 i = 1 i=1 i=1

最坏时间复杂度 O ( N ) O(N) O(N):即 i = n i=n i=n

平均复杂度 O ( N ) O(N) O(N)


在这里插入图片描述

代码

//按序号取值 
LNode* GetElem(
    LinkList L,
    int i) {  // LNode *强调返回的是一个结点;Linklist L强调是一个单链表
  int j = 1;
  LNode* p =
      L->next;  // p和next一样都指向的是下一个结点,结点包含数据域和指针域
      //此时p存放的是下一个结点的地址
  if (i == 0)
    return L;  // 返回头结点
  if (i < 1)
    return NULL;  // 由于i此时不等于0,输入则为负数,故返回NULL
  while (p != NULL && j < i) {  // next指针不为空即非尾
    p = p->next;                // p->next 相当于(*p).next
    j++;
  }
  return p;
}
3.2.2 按值查找表结点

代码:

//按值查找表结点
LNode* LocateElem(LinkList L, Elemtype e) {
  LNode* p = L->next;
  while (p != NULL && p->data != e) {
    p = p->next;
  }
  return p;  //找到后返回该结点,否则返回NULL
}
3.2.3 遍历链表并输出(源自大话数据结构)

代码:

/* 初始条件:链式线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
bool ListTraverse(LinkList L) {
  LinkList p = L->next;
  while (p) {
    visit(p->data);//error: visit is invalid in c99,改成printf即可,但无法输出位序
    p = p->next;
  }
  printf("\n");
  return true;
}

3.3 插入

3.3.1 后插

最好时间复杂度为 O ( 1 ) O(1) O(1):即 i = 1 i=1 i=1

最坏时间复杂度 O ( N ) O(N) O(N):即 i = n i=n i=n

平均复杂度 O ( N ) O(N) O(N)

王道代码:
后插法

更改后的c语言代码:

//插入结点(按位序后插)在第i个位置插入
bool Insert_NextNode(LinkList* L, int i, Elemtype e) {
  if (i < 1)
    return false;                 //位序不合法
  LNode* p = GetElem(*L, i - 1);  //找到要插入位置的前一个结点
  LNode* s = (LNode*)malloc(
      sizeof(LNode));  //要给s分配内存空间,如果不分配的话会报错内存崩溃
  s->data = e;
  s->next = p->next;
  p->next = s;
  return true;
}

3.3.2 前插

查找前驱结点再插在后面则时间复杂度为 O ( N ) O(N) O(N)
设待插入结点为*s,将*s插入到*p的前面。我们仍然将*s插入到p的后面,然后将p->data与s->data交换,时间复杂度为 O ( 1 ) O(1) O(1)

指定结点的前插操作
指定结点的前插操作

bool InsetPriorNode(LNode* p, Elemtype e) {
  Elemtype temp;
  if (p == NULL)
    return false;  //不能在头结点前插入
  LNode* s = (LNode*)malloc(sizeof(LNode));
  s->data = e;
  s->next = p->next;  //修改指针域,不能颠倒
  p->next = s;        //将s连接到p之后
  temp = p->data;     //交换数据域
  p->data = s->data;
  s->data = temp;
  return true;
}

3.4 删除

3.4.1 按位序删除

在这里插入图片描述

//按位序删除(带头结点)
//先检查删除位置的合法性,后查找表中第i-1个结点,即被删结点的前驱结点,再将其删除。
bool Delete_by_order(LinkList* L, int i, Elemtype* e) {
  if (i < 1)
    return false;                 //删除位置不合法,不能删头结点
  LNode* p = GetElem(*L, i - 1);  //指针p为前驱结点
  LNode* q = p->next;             // q为被删结点
  *e = q->data;                   //用e返回删除的数据
  p->next = q->next;  //让p结点断开与q的连接并连接第i+1位
  free(q);            //释放q
  return true;        //删除成功
}
3.4.2 按结点删除

查找前驱结点再删除该结点则时间复杂度为 O ( N ) O(N) O(N)

与后继结点交换数据并删除后继结点时间复杂度为 O ( 1 ) O(1) O(1)

但如果被删除的是最后一个结点,该种方法不适用,最后一个结点只能找前驱!

//按结点删除
//查找前驱结点则时间复杂度为O(N)
//与后继结点交换数据并删除后继结点时间复杂度为O(1)
bool Delete_LNode(LNode* p, Elemtype* e) {
  if (p == NULL||p->next == NULL)
    return false;      //删除结点不合法;或被删除的是最后一个结点,该种方法不适用,最后一个结点只能找前驱!
  
  *e = p->data;        //用e返回删除的数据
  LNode* q = p->next;  // q为被删结点后继结点
  p->next = q->next;   //让p结点断开与q的连接并连接第i+1位
  free(q);             //释放q
  return true;         //删除成功
}


3.5 求表长

大话数据结构代码:

//获取链表长度
int ListLength(LinkList L) {
  int i = 0;
  LinkList p = L->next; /* p指向第一个结点 */
  while (p) {
    i++;
    p = p->next;
  }
  return i;
}

更改后的代码:(增强了健壮性)
如对空表进行访问头结点next操作时将报错code=1,意为访问了已经释放的空间。

//获取链表长度
int ListLength(LinkList L) {
  int i = 0;
  if (L->next == NULL) {
    return 0;
  }
  LinkList p = L->next; /* p指向第一个结点 */
  while (p) {
    i++;
    p = p->next;
  }
  return i;
}

*调用和检验

int main() {
  LNode* L;  //声明一个单链表(头指针)
  //此时系统自动分配一个头结点+首元节点,头结点data为0,next指向首元节点;首元节点data为空,next为null
  printf("%d\n", Empty(L));  //此时显示非空表,因为头结点L->next不为null

  InitList(&L);  //初始化一个空表
  /*初始化传入的是头指针的地址,形参如果是Linklist
   * L,则是一级指针;但我们要改的是头指针指向的头结点,那么要用的是二级指针Linklist
   * *L*/
  /*问题:为何不可传L->next入一级指针LinkList L?
  答:首先next是个指针,指针存放的是下一个结点的地址,指针作为实参传给指针形参,指针形参获得的是下一个结点的地址,更改的是下一个结点内容*/
  /*再问:那么可否传入&(L->next)?
  再答:这里传入的是存放下一个结点地址的指针next的地址,无法更改头结点data,一级指针也无法更改下一个结点内容*/

  printf("%d\n", Empty(L));  //此时显示是空表

  L = List_headInsert(&L);
  CreateListHead(
      &L, 4);  //如果该函数建立头结点,则之前输入的值存在的结点被丢弃,下同
  CreateListTail(&L, 3);
  printf("链表长度为%d\n", ListLength(L));
  L = List_TailInsert(&L);
  ListTraverse(L);  //遍历

  Insert_NextNode(&L, 1, 5);  //在第i个位置插入结点并传值,后插,已知位序
  for (int i = 0; i < ListLength(L) + 1; i++) {
    printf("链表第%d项是%d\n", i, GetElem(L, i)->data);
  }
  InsetPriorNode(GetElem(L, 1), 3);  //前插

  Elemtype e_deleted;                  //返回删除的值
  Delete_by_order(&L, 1, &e_deleted);  //按位序删除
  printf("%d\n", e_deleted);
  //按结点删除
  Delete_LNode(GetElem(L, 1), &e_deleted);
  printf("%d\n", e_deleted);

  return 0;
}

4. 指针的使用

定义数据类型时(回看定义定义了 LNode 和 *LinkList

*LinkList L <==> LNode *L (意为等价)
在main()中声明了头指针
LNode *L;
以「初始化中的指针使用 」为例,辨析下列几种情况:

头指针辨析

//初始化一个单链表(带头结点)
bool InitList(LinkList* L) {
  *L = (LNode*)malloc(sizeof(LNode));  //分配一个头结点
  if (*L == NULL)
    return false;     //分配失败
  (*L)->next = NULL;  //头结点后暂时没有结点
  return true;
}

int main() {
  LNode* L;  //声明一个单链表(头指针)
  //此时系统自动分配一个头结点+首元节点,头结点data为0,next指向首元节点;首元节点data为空,next为null
  printf("%d\n", Empty(L));  //此时显示非空表,因为头结点L->next不为null

  printf("%d\n", InitList(&L));  //初始化一个空表
    //初始化传入的是头指针的地址,形参如果是Linklist L,则是一级指针;但我们要改的是头指针指向的头结点,那么要用的是二级指针Linklist *L
  printf("%d\n", Empty(L));  //此时显示是空表

声明头指针时,
LNode* L;
此时系统自动分配一个头结点+首元节点,头结点data为0,next指向首元节点;首元节点data为空,next为null


初始化以清除有可能的脏数据
InitList(&L)
此处实参为L头指针的地址,形参为LinkList *L二级指针。(相当于下面的LinkList * L_inner )


假设:
LNode头结点地址为0xA0
L头指针地址为0xB0
LinkList * L_inner 地址为0xC0
也即:
&LNode = 0xA0
&L = 0xB0
&L_inner = 0xC0

此时,令

LinkList *L_inner = &L;

LinkList *L_inner;
Linner = &L;

则:
L = &LNode = 0xA0
*L_inner = L = &LNode = 0xA0
**L_inner = *L = LNode

由于LinkList *L取得头指针的地址,所以在函数作用域中(*L)可以直接指向头结点的地址,对其进行的操作不会因函数结束内存释放,可以带出去


但注意!函数中(*L)->next 不同于 *L->next


前者:
1.先对L一级解引用 *
2.后再 -> 解引用找到头结点next指针域
后者则是
1.先去找二级指针L的成员next
(显然此时L的data存放的是头指针的地址,next是null)
2.然后对有可能存在的next中存放的地址进行解引用
(当然会报错,因为null不能被解引用)


问题:为何不可传L->next入一级指针LinkList L?


试答:首先next是个指针,指针存放的是下一个结点的地址,指针作为实参传给指针形参,指针形参获得的是下一个结点的地址,更改的是下一个结点内容


再问:那么可否传入&(L->next)?


再答:这里传入的是存放下一个结点地址的指针next的地址,无法更改头结点data,一级指针也无法更改下一个结点内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值