C语言学习笔记(浙大翁恺版)第十四周 链表

目录

14.1.1 可变数组

array.h

array.c

array.c main

14.2.1 可变数组的缺陷(2个)

14.2.2 链表

Node.h

linked-list.c

14.2.3 链表的函数

方法一:错误的示例

方法二:有关最后的head = p; 

方法三:传head的指针进去 

方法四:创建一个List结构体代表整个链表

14.2.4 链表的一些操作

遍历链表

搜索链表(在搜索基础上更进一步)

链表删除(搜索到了才能删除)

清空链表


14.1.1 可变数组

首先我们想一想这个问题需要的特点(功能):

  • 可增长
  • 能获得当前大小
  • 可以访问其中的单元

解决方法:设立函数库,几个函数分别实现不同功能

array.h

#ifndef _ARRAY_H
#define _ARRAY_H

typedef struct{
    int *array;
    int size;
}Array;//重命名结构为ARRAY

}*Array
很多人第三方库会设定成一个指针,下方的参数表里经常用到,
可以将参数表里的*去掉了,但是这样的坏处是你之后就不能访问
本地变量array(会动态创建一个),而且可读性也不好

Array array_create(int init_size);//创建数组
void array_free(Array *a);//释放数组的内存空间
int array_size(const Array *a);//获得数组空闲大小
int* array_at(Array *a, int index);//访问数组某个单元
void array_inflate(Array *a,int more_size);//让数组变大

array.c

#include"array.h"

//typedef struct{
//    int *array;
//    int size;
//}Array;

const BLOCK_SIZE = 20;//设自己觉得合适的大小

Array array_create(int init_size)
{
    Array a;//创建结构变量a
    a.size = init_size;
    a.array = (int*)malloc(sizeof(int)*a.size);
    //初始化大小和指针
    return a;
//返回a本身而不是指针,此时制作的是本地变量,因此可以在main函数中灵活使用
}

void array_free(Array *a)//上个函数中的结构里的*array需要释放
{
    free(a->array);
//为了保险起见,防止free两次(freeNULL是无害的),把这两个也初始化为0
    a->array = NULL;
    a->size = 0;
}

//既然只是想获得size,为什么不干脆printf里return a.size?还写函数干什么
//实际上,我们用函数将内部的实现细节封装起来,防止在大体量程序里直接调用带来的问题
//因为可能根本无法调用
int array_size(const Array *a)
{
    return a->size;
}

//为什么返回指针?因为这样做可以同时做到对指针的读和写
eg:*array_at(&a,0) = 10;//写入
    printf("%d\n",*array_at(&a,0));//读取
当然,写两个分开的函数也是可以的

int* array_at(Array *a, int index)
{
    return a->array[index];
    这样对吗?并不,注意这个函数的返回类型是个int指针,我们这样写返回的是int
    return &(a->array[index]);
}
//我们动态分配的内存空间实际上是不能长大的,是固定的
//所以思路是:重新分配一块空间,把原来空间上的东西复制到新的大空间
void array_inflate(Array *a,int more_size)
{
    int *p = (int*)malloc(sizeof(int)*(a->size + more_size));
    int i;
    for(i=0;isize;i++){
        p[i] = a->array[i];
    }
//这个循环可以用库函数里的memcpy代替,理论效率更高
    free(a->array);
    a->array = p;
    a->size += more_size;
}

array.c main

int main()
{
    Array a = array_create(100);//创建结构a,数组大小为100
    printf("%d\n",array_size(&a));//打印数组大小
    *array_at(&a,0) = 10;//设置a[0]=10
    printf("%d\n",*array_at(&a,0));//打印a[0]

    int number = 0;
    int cnt = 0;
    //设置输入-1为退出条件
    while(number != -1){
        scanf("%d",&number);
        if(number != -1){
            *array_at(&a,cnt++) = number;
        }
    }
    array_free(&a);
    
    return 0;
}

此时可以实现不断输入数值填满数组,但是当数组越界时,还需要对程序进行一些修改

int* array_at(Array *a, int index)
{
    if(index >= a->size){
        array_inflate(a, index - a->size + 1);
      //因为每次读入一个数,所以就让数组上标增长1,
      //然而每次增长1都要重新申请,然后完整复制之前的数据,
      //因此我们定一个单位,每次越界都增长一个单位的大小,这样更加高效

        array_inflate(a, (index/BLOCK_SIZE + 1)*BLOCK_SIZE - a->size);
    //比如我要访问102,block大小为20,现在只有100,也就是5个block,
    //那么102/20=5,5+1=6,6*20=120,120-100=20,于是函数知道要增长20
    //如果越界更多,用这个公式也可以一次性算好
    }
    return &(a->array[index]);
}

14.2.1 可变数组的缺陷(2个)

  • 拷贝需要时间,随着数组变大,每次拷贝需要的时间也会增加
  • 有足够内存,但不能够继续申请

C语言学习笔记(浙大翁恺版)第十四周 链表_第1张图片

 所以有什么更好的方式呢?

我们可以从申请一块新的内存,转为申请一块block(固定大小)的内存,然后再将它们连接起来,越界后告诉你该去哪里存储或读取,这样不需要拷贝数据额外增加时间,也解决了内存问题。

14.2.2 链表

 以int类型的数组为例,如何实现多个数组的连接呢?

C语言学习笔记(浙大翁恺版)第十四周 链表_第2张图片

一种思路是这样的:将每个单元分成两部分,前面是数据,后面是一个指针,指向下一个单元的数据。在开始和末尾的单元还要做个标记来确定整个数组的大小。

 就有这样的东西

C语言学习笔记(浙大翁恺版)第十四周 链表_第3张图片

那么在程序中如何实现呢?首先是结点

Node.h

#ifndef _NODE_H_
#define _NODE_H_

//结点,并重命名为Node
typedef struct _node{
    int value;
    struct _node *next;//指向下一个的指针(因为下一个是同类型)
//为什么不直接Node *next呢?因为这时候Node还没生成呢
}Node;

#endif

现在有一个使用场景,就是我不知道有多少份数据,但是我要一个一个录入,那么我读到第一个数据,怎么能让它出现在内存的第一部分,更进一步说,怎么能存放到链表头?

linked-list.c

#include"node.h"
#include
#include

//typedef struct _node{
//    int value;
//    struct _node *next;
//}Node;

int main()
{

//创建一个指针作为head,指向NULL(啥也不指)
    Node *head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);

        //先创建一个Node结构体,add to linked-list
        Node *p = (Node*)malloc(sizeof(Node));
        p->value = number;
        p->next = NULL;
//首先给*p分配内存,然后p所指的value接收传入的number,p所指的next指向NULL

        //然后找到最后的单元,find the list
        Node *last = head;//先指向头,从头开始遍历找尾
//这里的循环,当数组只有头时,last=head=NULL,结束条件是无效的,
//所以要判断一下last是不是NULL

    if(last){
            while( last->next ){//如果last还指向一个后继
                last = last->list;//让被指的元素成为last
                }

            //链接新增单元和原来的最后一段单元,attach
            last->next = p;//让新增的p成为尾
            }else{
            head = p;
            //如果条件不成立,last指向NULL,说明现在链表是空的,
            //就让head指向第一个单元
            }    
    } while (number != -1);

    return 0;
}

 C语言学习笔记(浙大翁恺版)第十四周 链表_第4张图片

14.2.3 链表的函数

知道了如何实现链表,我们在更多使用场景是将其封装成一个函数来使用,这里面也有一些问题需要注意

方法一:错误的示例

#include"node.h"
#include
#include

//typedef struct _node{
//    int value;
//    struct _node *next;
//}Node;

void add(Node* head,int number);
int main()
{
    Node *head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(head,number);//调用函数
            }    
    } while (number != -1);

    return 0;
}

void add(Node* head,int number)
{   
    // add to linked-list
    Node *p = (Node*)malloc(sizeof(Node));
    p->value = number;
    p->next = NULL;

    // find the list
    Node *last = head;
    if(last){
        while( last->next ){
        last = last->list;
     }
    // attach
        last->next = p;
     }else{
        head = p;
    
}

方法二:有关最后的head = p; 

    在函数里会对head进行修改,但是属于本地变量,因此传进来的head不会有变动
    一个解决方法是,把头指针Node* head定义成全局变量
    然而全局变量导致程序只能处理本次的数据
    那我返回一个head,并且调用函数时候写为head = add(head,number),可行吗?这当然是有进步的
    然而,如果忘记写head=,对一个空链表使用原来的函数就会导致程序出错,于是我们有第三种方案:
    传head的指针进去! 

方法三:传head的指针进去 

#include"node.h"
#include
#include

//typedef struct _node{
//    int value;
//    struct _node *next;
//}Node;

Node* add(Node* head,int number);
int main()
{
    Node *head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(head,number);
            }    
    } while (number != -1);

    return 0;
}

Node* add(Node** pHead,int number)//这里用Node** pHead
{   
    // add to linked-list
    Node *p = (Node*)malloc(sizeof(Node));
    p->value = number;
    p->next = NULL;

    // find the list
    Node *last = *pHead;//这里*pHead
    if(last){
        while( last->next ){
        last = last->list;
     }
    // attach
        last->next = p;
     }else{
        head = p;
     }
}

方法四:创建一个List结构体代表整个链表

好处:给改进程序带来很多可能

#include"node.h"
#include
#include

//typedef struct _node{
//    int value;
//    struct _node *next;
//}Node;

typedef struct _list{
    Node* head;
//在这个基础上可以扩充功能,比如加一个尾指针代替遍历的部分
}List;

void add(Node* head, int number);
int main()
{
    List list1;//创建一个结构体并初始化
    list1.head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(&list1,number);
            }    
    } while (number != -1);

    return 0;
}

void add(List* pList,int number)//传入的是List指针,且函数返回类型void
{   
    // add to linked-list
    Node *p = (Node*)malloc(sizeof(Node));
    p->value = number;
    p->next = NULL;

    // find the list
    Node *last = pList->head;//list里的head
    if(last){
        while( last->next ){
        last = last->list;
     }
    // attach
        last->next = p;
     }else{
        pList->head = p;//p=plist指向的head
     }
}

14.2.4 链表的一些操作

遍历链表

想要输出遍历进去的数据,怎么做呢?

int main()
{
    List list1;
    list1.head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(&list1,number);
            }    
    } while (number != -1);
//创建指针
    Node *p;
//遍历输出
    for(p=list.head; p ;p=p->next)
    {
        printf("%d\t",p->value);
    }
    printf("\n");
    return 0;
}

然后将其也封装成一个函数

void print(List *pList);
int main()
{
    List list1;
    list1.head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(&list1,number);
            }    
    } while (number != -1);

    print(&list);
    return 0;
}
void print(List *pList){
    Node *p;
    for(p=list.head; p ;p=p->next)
    {
        printf("%d\t",p->value);
    }
    printf("\n");
}

搜索链表(在搜索基础上更进一步)

void found(List *pList);
int main()
{
    List list1;
    list1.head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(&list1,number);
            }    
    } while (number != -1);

    scanf("%d",&number);
    found(&list,number);
    return 0;
}
void print(List *pList,int number){
    Node *p;
    int isFound = 0;
    for(p=list.head; p ;p=p->next)
    {
        if(p->value == number){
            printf("找到了!\n");
            isFound = 1;
            break;
        }
        
    }
    if(!isFound){
        printf("没找到!\n");
    }
}

链表删除(搜索到了才能删除)

如果要删除的链表不是头也不是尾:

  • 要将删除部分前一个单元的指针指向后一个单元
  • 释放掉删除的链表

问题来了,我们有指针指向下一个,但没有指向前面的,如何获得前面单元的指针呢?

如果是双向链表,这个问题就迎刃而解,所以我们仿照它做一个吧!

 步骤:

  1. 创建遍历指针p和另一个指针q
  2. 找到要删除的数,用p表示
  3. 让q指向p的前一个单元
  4. q的下一个由指向p转为指向p的下一个,q.next -> p.next
  5. free(p)
int main()
{
    List list1;
    list1.head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(&list1,number);
            }    
    } while (number != -1);


    scanf("%d",&number);    
    Node *q;
    for(q=NULL,p=list.head; p ;q-p,p=p->next)//两个指针同时往下移一位
    {    
        if(p->value == number){
            if(q){
                q->next = p->next;
            }else{
                list.head = p->next;
            }    
            free(p);
            break;
        }
    }
    return 0;
}

在使用指针的时候,出现在->左侧的指针代表你要访问指针所指的值了,所以这个指针不能指向NULL,这时候就要仔细检查下它是不是NULL,有没有足够的代码检查。

像这个程序中,for循环的条件里就是p,如果是NULL则会退出循环,然而q没有保证,这就是程序的边界条件。

如果p在第一个就找到了,q指向的是NULL,要执行删除时,我们不是让q的next指向p的next,而是让head指向p的next。所以这里需要一个额外的判断条件,当q存在,也就是不指向NULL时才让q->next=p->next;否则的话让head指向p->next

C语言学习笔记(浙大翁恺版)第十四周 链表_第5张图片

 

清空链表

链表中所有的结点都是malloc分配的,当你想要删掉整个链表,要怎么做?

  1. 让p指向head
  2. 让q指向p的下一个结点
  3. 释放p
  4. 让p=q
  5. 当p指向NULL,说明删完了

 

int main()
{
    List list1;
    list1.head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if(number != -1);
            add(&list1,number);
            }    
    } while (number != -1);
   
    Node *q;
    for(p=head; p ;p=q){
        q = p->next;
        free(p);
    }
    return 0;
}

你可能感兴趣的:(C语言,链表,c语言,学习)