【Conyrol】链表简介及应用

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/Conyrol/article/details/84673008

本文后续可能还会更新一些其他使用链表的情况和处理方式

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成——摘自百度百科

目录

链表简介

看起来链表的简介十分复杂 XD
实际上用数组对比一下,就能明白其中的奥秘

  1. 数组的各个元素在物理储存单元上是连续的,顺序的,通常是一整片储存空间存着一个数组
    链表的各个节点之间用指针联系,每个节点在物理储存单元上可以是分散的
  2. 数组的空间开辟是静态的,通常需要提前告诉编译器我要开多大的空间,开完之后很难更改,很多时候会造成空间的浪费
    链表的空间开辟是动态的,它可以在程序运行中时刻添加节点
  3. 数组的各个元素可以用数组下标来引索
    链表的各个节点只能用指针来引索
    比如说我要读取第三个元素,数组可以直接 A[2] 调用,而通常情况下链表只能从头指针找下二个节点,从第二个节点找第三个节点来调用

链表的构造和操作

说了这么多,不懂链表的一定是一脸懵逼,那么链表应该怎么构造呢?
首先对于链表来说,每个节点最重要的莫过于两点

  • 存储的数据
  • 指向下一个节点的指针

为什么说这两个最为重要呢?数据不用说了,所谓指针有什么用,让我们看看数组,对于数组来说,每次我们可以用数组下标来调用某个元素,但链表不行!
我们前面说过,链表是由指针连接的,意思就是每个节点都有一个指针,它指向下一个节点,如果我想遍历到某个节点,往往需要从头指针开始遍历链表,一点点向后跟着节点里的指针走
所以说,一个节点是储存两个值的!一个是数据,一个是指向下一个节点的指针
这时候就要用到struct

struct Node{             //用struct表示的节点
    int Data;            //存储的数据
    struct Node* Next;   //指向下一个节点的指针,因为指针是指向struct的,所以用这种前缀
};
//但是实际上每次我们每次生成或者调用都要写struct Node,不如我们用 typedef 把它定义成 node
typedef struct Node{
    int Data;
    struct Node* Next;   
    //注意上面还是要写struct Node的,不然会报错,因为编译器这时候还不知道你把它设为了node
}node;                   //用 node 代替 struct Node 

链表构造

首先我们在构造节点时需要 malloc 函数,调用这个函数可以给你在内存中分配一块内存,并把这片内存的首地址返回给你
具体用法就是 malloc( 要分配的内存大小 )
所以说我们可以用

node *p = (node *)malloc(sizeof(node));

分配出一块空间给我们的数据结构 node,当作一个节点,并把指向这个数据结构的指针返回给指针 p

代码如下:

node *Head;                      
//声明一个头指针,一会让它指向第一个节点,它的作用是当我们构造完链表,我们能有一个指针找到并遍历我们的链表
node *Build(int n){     //这样写意思是,让Build函数的返回值是一个指向node的指针
    node *p1, *p2;
    Head = (node *)malloc(sizeof(node)); 
//分配空间,Head 指针和 p1 指针指向第一个节点,注意,在构造过程中 Head 指针是不变化的
//在这里头指针指向的节点不存数据,仅作为用来引索的头部,也可以让头节点存数据
    for(int i=0; i<n; i++){
        if(!i) p1 = Head; //如果 i=0 就让 p1 指向头节点
        p2 = p1;
        p1 = (node *)malloc(sizeof(node));
        cin>>p1->Data;
        p2->Next = p1;   //上一个节点中的指针指向当前的这个节点
    }
    p1->Next = NULL;     //让尾节点中的指针指向 NULL 作为结束
    return p1;           //返回尾指针
}

PS:之所以选择头节点不存数据,是因为这样后面的插入,删除,排序等都比较好写

读取,插入和删除

读取:

node* Read(int A){         // A 是引索值,跟数组下标一样 A = 3 对应第三个节点
    node *p = Head;
    for(int i=0; p->Next!=NULL && i!=A; i++) p = p->Next;
    //如果引索值 A 超过链表,或者找到了引索值 A 对应的位置,就跳出
    return p;
    //返回指向 A 对应引索值的那个节点的指针,如果要表示第三位数据可以用 Read(3)->Data;
    //如果引索值过大,那就是只会返回指向最后一个节点的指针
}

没错,读取一个指定的数据是 O(n) 的,不像数组是 O(1)
插入:

node* Insert(node *p){       // 这个函数的意思是在指针 p 所指向的节点后面插入一个新节点
    node* p1 = (node *)malloc(sizeof(node));
    node* p2 = p->Next;
    p->Next = p1;
    p1->Next = p2;
    return p1;
//返回指向这个新节点的指针,你可以用 Insert( read(0) )->Data = 3 来在链表的的头部插入一个新节点,储存数据为 3
}

删除:
删除链表中的某个值我们利用free函数

free( 指向数据结构的指针 );

可以释放 ( 删除 ) 那个指针指向的数据结构

void Delete(node *p){       // 这个函数的意思是删除指针 p 对应的节点
//比如说 Delete(read(1)) 就会删除第一个储存数据的节点
//而 Delete(Read(0)) 则会删除头节点,之后你就再也找不到这个链表了XD
    node* p1 = p->Next->Next;
    free(p->Next);             //删除此节点
    p->Next = p1;              //让上一个节点指向下下个节点
}

打印:
额外的代码,用来打印一边链表

void Print(){
    node* p = Head->Next;
    cout<<p->Data<<" ";
    while(p->Next!=NULL){
        p = p->Next;
        cout<<p->Data<<" ";
    }
}

链表在排序算法中的应用

单向链表可以做到的排序

至于排序算法可以看【Conyrol】排序算法
在哪里的排序都属于以数组为基础的排序,跟本文链表排序可以对应着去看

首先我们需要明白单向链表能做些什么,它跟数组的区别:

  1. 遍历数据的单向性,我通常只能从头向尾遍历,也就是单方向遍历
  2. 不能跳跃遍历,不像数组可以用下标1,3,5,7这样遍历数据,链表只能一个个节点遍历

下面就是单向链表能做到的排序(实际上基本所有排序都可以用链表和他的衍生数据结构来做)

  • 选择排序,设一个 i 指针指向第一个有数据的节点,每次向后遍历把最小的节点跟 i 指向的节点交换,然后 i = i->Next 保证 i 不断在向后移动
  • 冒泡排序,这个不用说了,单向的
  • 插入排序,虽然数组插入排序外层循环向后遍历,里面循环向前遍历找合适的位置插入,但是我们也可以在里面循环向后遍历找指定位置
  • 希尔排序,跳跃式遍历,这种排序链表做不到
  • 鸡尾酒排序,既有向前遍历,又有向后遍历,这种需要双向链表
  • 快速排序,看似这种用到了二分的排序不行,但实际上我们有前后指针式快排,为链表量身定制!
  • 归并排序,跟快排同理,也可以做到
  • 堆排序,这个用单向链表做不到,不过用链表来模拟二叉树这种结构就很轻松

选择排序

基本跟数组的那种一模一样
至于为什么最开始
node *i = Head->Next
是因为我的链表设计的是头指针不存数据,下一个节点存第一个数据,头指针指向的节点仅当做一个检索,这样写插入删除时比较方便,不用特判头尾什么的

void SelectionSort(){ 
    for(node *i = Head->Next; i!=NULL; i = i->Next){
    	node *jShu = i;
    	for(node *j = i; j!=NULL; j = j->Next) if(j->Data<jShu->Data) jShu = j;
        swap(jShu->Data, i->Data);
    }
}

冒泡排序

这种链表实现的冒泡排序比数组实现的要慢,大家可以尝试优化一下

void BubbleSort(){
    for(int i=0; i<n; i++) //在这里这层循环作用是计数,记录我排好了多少个元素
        for(Node *j = Head->Next; j->Next!=NULL; j = j->Next)
            if(j->Data > j->Next->Data) swap(j->Data, j->Next->Data);
}

插入排序

跟数组插入排序一样,只不过这个是第二层循环从前往后找合适的位置插入,而数组排序是从后往前,更合理一些,这里用到了前面定义的 Delete() 和 Insert() 函数来做到插入步骤 (但实际上可以简化!)

void InsertionSort(){
    Node *i = Head->Next;
    while(i->Next!=NULL){
        bool B = 1;
        int Zhi = i->Next->Data;
        for(Node *j = Head; j!=i; j = j->Next){
            if(j->Next->Data > Zhi){
                Delete(i);
                Insert(j)->Data = Zhi;
                B = 0;
                break;
            }
        }
        if(B) i = i->Next;
    }
}

下面就是简化版本,我们可以不删除节点,而是把节点的指针指向改一下,这样就省去了删除节点和给新节点分配空间的过程

void InsertionSort2(){
    Node *i = Head->Next;
    while(i->Next!=NULL){
        bool B = 1;
        Node *p = i->Next;
        for(Node *j = Head; j!=i; j = j->Next){
            if(j->Next->Data > p->Data){
                i->Next = p->Next;
                p->Next = j->Next;
                j->Next = p;
                B = 0;
                break;
            }
        }
       if(B) i = i->Next;
    }
}

最后就是实际上这种链表插入并没有数组形式的插入快XD

快速排序

使用前后指针法进行快速排序,实际上如果是双向链表也可以使用左右指针法
详情可以看下面链接的快速排序部分
【Conyrol】排序算法

void QuickSortA(node* first, node* end){
    if(first==NULL||first->Next==end||first==end) return;
    node* Cur = first->Next;
    node* Pre = first;
    int Bet = Pre->Data;
    while(Cur != end){   
        if (Cur->Data < Bet){
            Pre = Pre->Next;
            if(Pre != Cur) swap(Cur->Data, Pre->Data);
        }
        Cur=Cur->Next;
    }
    swap(first->Data, Pre->Data);
    QuickSortA(first, Pre);
    QuickSortA(Pre->Next, end);
}

值得注意的是在基准值选取上,取中值和三值优化在链表里很难做,但其它优化基本都可以实现

猜你喜欢

转载自blog.csdn.net/Conyrol/article/details/84673008