针对在校大学生的C语言入门学习——链表

针对在校大学生的C语言入门学习——链表

  • 看到标题可能大家会觉得我怎么说话不算,不是说好的这次要给大家带来《学生管理系统》项目吗?其实我没有食言,只是整个项目太大了,一篇文章很难说完。
  • 为什么这次要聊一聊链表呢,因为《学生管理系统》肯定是要对学生的数据进行各种操作。既然涉及到了大量的数据,选择一个合适的数据结构来存放数据就是这个项目成功的关键。无论是从实际效果考虑,还是以大家的学习基础考虑,链表都是一个非常不错的选择。
  • 今天给大家带来一个我对链表的封装,链表是所有数据结构中最好实现的数据结构。对数据结构的封装也都是围绕增、删、改、查四大功能,下面我们开始。

结构体封装

/*
双向链表节点结构体
*/
typedef struct Node
{
     
    void* data;
    struct Node* next;
    struct Node* pre;
}Node;

/*
双向链表结构体
*/
typedef struct Link
{
     
    Node* head;
    Node* tail;
    int size;
}Link;

  • Node结构体是链表节点的结构体封装,可以看出我使用的是一个双向链表。data之所以定义成void*类型,因为我的链表可以存放任何类型数据。void*在C语言中是泛型。
  • Link结构体是我对链表的封装,每创建一个链表都会有一个Link实例与之对应。在Link中我分别保存了头尾指针和链表的长度。

创建链表

/*
创建链表中一个节点
*/
Node* createNode(void* data)
{
     
    Node* node = (Node*)malloc(sizeof(Node));
    node->data = data;
    node->pre = NULL;
    node->next = NULL;
    return node;
}

/*
创建链表实例
*/
Link* createLink()
{
     
    Link* l = (Link*)malloc(sizeof(Link));
    l->head = createNode(NULL);
    l->tail = l->head;
    l->size = 0;
    return l;
}

  • createNode函数是创建链表中的一个节点,参数data是这个节点中存放的数据。节点要在堆空间创建。大家看我前面的内容就会知道,堆空间的变量生命周期是自定义的。非常适合保存大量数据。既不会因为栈空间生命周期过短而丢失数据,也不会因为静态空间生命周期过长而浪费内存。每个节点创建出来都会默认把next和pre指针置NULL。野指针是不允许出现的。
  • createLink函数是创建一个链表实例,初始时候会有一个无数据的节点作为头节点,head和tail指针都指向这个空头。以后添加新节点的时候会移动tail指针。
    针对在校大学生的C语言入门学习——链表_第1张图片
  • 上图就是我这个双向链表的结构。另外建议大家在写链表的时候多画图,因为链表的编码需要大量的指针操作,稍微不慎就会指向错误。新手写链表时边写边画是一个好办法。

删除链表

/*
删除链表
*/
void deleteLink(Link* l)
{
     
    Node* node = l->head;
    while(node != NULL)
    {
     
        Node* t = node;
        node = node->next;
        free(t->data);
        free(t);
    }
    free(l);
}
  • 删除链表的思路就是遍历每个节点,然后逐个释放。但是有一个问题要注意,一旦节点被释放了,那么指向节点的指针就是野指针。我们不能通过野指针移动到下一个节点。所以函数中定义了指针t用来指向要删除的节点。然后先移动,再删除节点。free(t->data);是释放节点中的数据。free(t);是释放当前节点。两个free缺一不可,避免造成内存泄漏。最后释放整个链表实例。

/*
向链表中添加新节点
*/
void addNode(Link* l, Node* node)
{
     
    l->tail->next = node;
    node->pre = l->tail;
    l->tail = node;
    l->size++;
}

/*
向链表中添加新数据
*/
void addItem(Link* l, void* data)
{
     
    Node* node = createNode(data);
    addNode(l, node);
}

  • addNode函数是向链表中添加一个新的节点(Node实例),参数1是插入链表的指针,参数2是插入节点的指针。这个插入就是向链表尾部插入。我没有做在任意位置插入节点的函数,大家需要的话可以自己编写。需要注意的是向尾部插入节点后一定要改变tail指针的指向,tail始终指向链表中最后一个节点。这个函数因为使用了Node类型,所以我不会在头文件中声明addNode,不让用户使用这个函数。数据结构的封装一定要隐藏自己的结构信息,不给用户胡乱操作的机会。
  • addItem函数是我想让用户使用的函数,因为用户只需要传递链表实例指针和数据指针就可以了。
  • 注意:C语言是面向过程的语言,想像面向对象语言那样封装的滴水不漏是不可能的,这里我们尽力而为就好了。

/*
获取指定索引位置的数据
*/
void* getItem(Link* l, int index)
{
     
    Node* node = l->head->next;
    int i;
    for(i = 0;i < index;i++)
    {
     
        node = node->next;
    }
    return node->data;
}
  • getItem函数是通过遍历链表的方式来获取指定索引位置的数据。这是链表的天然缺陷,因为链表每个元素在内存中不连续,所以没有办法像数组那样以O(1) 的时间复杂度来获取指定索引位置的元素。
  • 现在出现了一个很严重的问题,如果用户使用getItem函数遍历链表的话,效率将是灾难性的。举例子,获得第一个数据需要遍历的节点个数是1,访问第二个时需要遍历的节点个数是2,依次类推。每访问一个元素都会把链表从头遍历一遍,反反复复浪费了很多时间。
  • 遍历链表最有效的方式就是使用指针遍历,但是我们为了链表结构的安全性,不允许用户获得链表中指向任何节点的指针。问题似乎卡在了这里。好在我们是站在巨人的肩膀上看问题,这个问题早已被大神破解。
  • 迭代器遍历

  • 迭代器遍历的思路就是把链表的节点指针再进行一层封装,就是迭代器实例。提供一个函数移动迭代器就可以了。请看下面代码。
/*
迭代器结构体
*/
typedef struct Iterator
{
     
    Node* node;
}Iterator;

/*
创建迭代器
*/
Iterator createIterator(Link* l)
{
     
    Iterator iter;
    iter.node = l->head->next;
    return iter;
}

/*
判断迭代器是否指向了链表尾部
*/
int iteratorEnd(Iterator iter)
{
     
    return iter.node==NULL;
}

/*
移动迭代器指向链表中下一个节点
*/
Iterator nextIterator(Iterator iter)
{
     
    Iterator newIter;
    newIter.node = iter.node->next;
    return newIter;
}

  • 通过这段代码你会发现迭代器的操作就是对指针node的操作,只不过这些操作不让用户直接完成,而是调用我提供的接口函数来完成。这样能保证结构安全。下面写一段代码来测试我们目前的链表性能。
#include 
#include "link.h"

int main(void)
{
     
    int arr[10] = {
     44,44,8,45,21,5,90,45,5,5};//准备好的数据
    Link* l = createLink();//创建链表实例
    int i;
    for(i = 0;i < 10;i++)
    {
     
        int* data = (int*)malloc(sizeof(int));//创建数据
        *data = arr[i];
        addItem(l, data);//数据添加入链表
    }

    Iterator iter;
    for(iter = createIterator(l);//创建链表l的迭代器实例
        !iteratorEnd(iter);//判断迭代器不指向链表尾
        iter = nextIterator(iter))//移动迭代器指向链表下一个元素
    {
     
        int* data = (int*)iteratorData(iter);//获取迭代器指向节点的数据
        printf("%d ", *data);
    }
    printf("\n");
    
    deleteLink(l);//删除链表
    return 0;
}
  • 测试代码中我使用int类型作为数据,这样简单又直观。link.h写我们定义过的结构体和函数的声明。测试逻辑是将数组中的数据分别插入链表,然后使用迭代器遍历链表并打印每个数据。以上就是我们查的部分,也是数据结构中最重要最难写的部分。

/*
以迭代器修改
*/
void updateItem(Iterator iter, void *data)
{
     
    free(iter.node->data);
    iter.node->data = data;
}

/*
以索引修改
*/
void updateItemByIndex(Link *l, int index, void *data)
{
     
    Iterator iter = createIterator(l);
    int i;
    for(i = 0;i < index;i++)
    {
     
        iter = nextIterator(iter);
    }
    updateItem(iter, data);
}
  • 修改我给用户提供两个接口函数。updateItem以迭代器修改是用在用户遍历链表的时候随时修改。updateItemByIndex以索引修改是方便用户修改指定的元素。修改要注意的是原来的数据一定要释放,然后Node的data指针指向新的数据。下面是一段测试代码。
#include 
#include "link.h"

int main(void)
{
     
    int arr[10] = {
     44,44,8,45,21,5,90,45,5,5};//准备好的数据
    Link* l = createLink();//创建链表实例
    int i;
    for(i = 0;i < 10;i++)
    {
     
        int* data = (int*)malloc(sizeof(int));//创建数据
        *data = arr[i];
        addItem(l, data);//数据添加入链表
    }
    
    int* data = (int*)malloc(sizeof(int));
    *data = 100;
    updateItemByIndex(l, 0, data);//把0位置的元素修改成100
    
    Iterator iter;
    for(iter = createIterator(l);
        !iteratorEnd(iter);
        iter = nextIterator(iter))
    {
     
        int* data = (int*)iteratorData(iter);
        if(*data == 45)//遍历过程中把所有值是45的数据修改为54
        {
     
            int* newData = (int*)malloc(sizeof(int));
            *newData = 54;
            updateItem(iter, newData);
        }
    }
    
    //重新创建l的迭代器实例遍历
    for(iter = createIterator(l);//创建链表l的迭代器实例
        !iteratorEnd(iter);//判断迭代器不指向链表尾
        iter = nextIterator(iter))//移动迭代器指向链表下一个元素
    {
     
        int* data = (int*)iteratorData(iter);//获取迭代器指向节点的数据
        printf("%d ", *data);
    }
    printf("\n");
    
    deleteLink(l);//删除链表
    return 0;
}

/*
以迭代器删除
*/
Iterator removeItem(Link* l, Iterator iter)
{
     
    Iterator newIter;
    newIter.node = iter.node->next;
    iter.node->pre->next = iter.node->next;
    if(iter.node->next != NULL)//尾部节点不能操作next指针
    {
     
        iter.node->next->pre = iter.node->pre;
    }
    free(iter.node->data);
    free(iter.node);
    l->size--;
    return newIter;
}

/*
以索引删除
*/
void removeItemByIndex(Link* l, int index)
{
     
    Iterator iter = createIterator(l);
    int i;
    for(i = 0;i < index;i++)
    {
     
        iter = nextIterator(iter);
    }
    removeItem(l, iter);
}
  • 删除和修改一样,我提供了两个接口函数。removeItem函数是以迭代器删除,用户在遍历时可以使用这种方式删除。removeItemByIndex函数用户可以删除指定索引的数据。需要注意的点比较多,首先是删除节点需要分别free(iter.node->data)和free(iter.node),链表的长度也要减1。迭代器删除节点后原来的迭代器就成为了野指针,所以在删除后我会返回一个指向删除节点下一个节点的迭代器实例。下面测试代码。
#include 
#include "link.h"

int main(void)
{
     
    int arr[10] = {
     44,44,8,45,21,5,90,45,5,5};//准备好的数据
    Link* l = createLink();//创建链表实例
    int i;
    for(i = 0;i < 10;i++)
    {
     
        int* data = (int*)malloc(sizeof(int));//创建数据
        *data = arr[i];
        addItem(l, data);//数据添加入链表
    }
    

    removeItemByIndex(l, 2);//删除索引是2的元素,就是原数据中的8
    
    Iterator iter = createIterator(l);
    while(!iteratorEnd(iter))     
    {
     
        int* data = (int*)iteratorData(iter);
        if(*data == 45)//删除值是45的数据
        {
     
            iter = removeItem(l, iter);
        }
        else
        {
     
            iter = nextIterator(iter);
        }
    }
    
    //重新创建l的迭代器实例遍历
    for(iter = createIterator(l);//创建链表l的迭代器实例
        !iteratorEnd(iter);//判断迭代器不指向链表尾
        iter = nextIterator(iter))//移动迭代器指向链表下一个元素
    {
     
        int* data = (int*)iteratorData(iter);//获取迭代器指向节点的数据
        printf("%d ", *data);
    }
    printf("\n");
    
    deleteLink(l);//删除链表
    return 0;
}
  • 遍历删除时需要注意的问题是如果删除了元素,那么使用removeItem的返回值就已经实现了迭代器的移动,此时不可以再使用nextIterator移动迭代器,会使得遍历跳项。

排序

  • 我们还有最后一个任务就是排序。因为链表不支持像数组一样的随机访问元素,所以链表能使用的排序方法很受限制。既然是因为元素不连续而受限制,那我们让其连续不就可以了吗!这里模仿hash映射的原理将链表的每个元素指针映射到一个数组中,然后对数据进行排序,可以使用任何高效的排序方法。排序完成后再根据数组顺序重组链表即可。下面代码我使用快速排序。
/*
快速排序算法
*/
void sortArr(Node** arr, int start, int end, int(*cmp)(void*, void*))
{
     
    if(start >= end)
        return;
    int left = start;
    int right = end;
    void* mv = arr[(start+end)/2]->data;
    while(left < right)
    {
     
        while(left<right && cmp(mv, arr[left]->data)==1)
        {
     
            left++;
        }
        while(left<right && cmp(mv, arr[right]->data)!=1)
        {
     
            right--;
        }
        if(left<right)
        {
     
            Node* t = arr[left];
            arr[left] = arr[right];
            arr[right] = t;
        }
    }
    sortArr(arr, start, left-1, cmp);
    sortArr(arr, left+1, end, cmp);
}

/*
链表排序
*/
void sort(Link* l, int(*cmp)(void*, void*))
{
     
    int size = l->size;
    Node** arr = (Node**)malloc(sizeof(Node*)*size);
    Node* node = l->head->next;
    int i;
    //链表映射到数组
    for(i = 0;i < size;i++)
    {
     
        arr[i] = node;
        node = node->next;
    }
    sortArr(arr, 0, size-1, cmp);
    //根据数组重组链表
    l->tail = l->head;
    l->size = 0;
    for(i = 0;i < size;i++)
    {
     
        addNode(l, arr[i]);
    }
    l->tail->next = NULL;
    free(arr);
}
  • 快速排序的算法我不多说。排序首先我在堆空间创建一个和链表等长的数组,数组用来存放每个节点的指针。因为数组元素是指针,所以指向这个数组的指针就得定义成二级指针。关于这个问题可以参考我前面的《针对在校大学生的C语言入门学习——高级语法》。
  • 因为我的链表是一个泛型链表,所以我不可能知道用户存放的是什么类型的数据。但是排序就得比较,不知道数据类型怎么比较呢?我的解决方案就是定义函数指针。我只是把排序算法写好,至于数据的比较方法有用户来提供。也就是说用户需要自己写比较的函数,然后传递给我的函数指针参数。请看下面测试代码。
#include 
#include "link.h"

int cmp(void* num1, void* num2)
{
     
    int* a = num1;
    int* b = num2;
    if(*a > *b)
    {
     
        return 1;
    }
    else if(*a < *b)
    {
     
        return -1;
    }
    else
    {
     
        return 0;
    }
}

int main(void)
{
     
    int arr[10] = {
     44,44,8,45,21,5,90,45,5,5};//准备好的数据
    Link* l = createLink();//创建链表实例
    int i;
    for(i = 0;i < 10;i++)
    {
     
        int* data = (int*)malloc(sizeof(int));//创建数据
        *data = arr[i];
        addItem(l, data);//数据添加入链表
    }
    sort(l, cmp);

    Iterator iter;
    for(iter = createIterator(l);//创建链表l的迭代器实例
        !iteratorEnd(iter);//判断迭代器不指向链表尾
        iter = nextIterator(iter))//移动迭代器指向链表下一个元素
    {
     
        int* data = (int*)iteratorData(iter);//获取迭代器指向节点的数据
        printf("%d ", *data);
    }
    printf("\n");
    
    deleteLink(l);//删除链表
    return 0;
}

总结

  • 链表虽然不难,但其数据结构的本质很考验一个程序员的功力。同学们阅读或者编写链表代码时如果思路不能畅通,一定要边写边画。假以时日便能信手拈来。我的链表就封装到这里,后面的学生管理系统看它大显身手吧!本课源码下载点击。
    -

你可能感兴趣的:(C语言教学,链表,数据结构,java,算法,python)