zoukankan      html  css  js  c++  java
  • 单链表的各项常规操作

    对于单链表的头指针,头结点的说明:
    单链表也是一种线性表,所以总得有个头有个尾。链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。

    这里有个地方要注意,就是对头指针概念的理解,这个很重要。“链表中第一个结点的存储位置叫做头指针”,如果链表有头结点,那么头指针就是指向头结点数据域的指针,参考下图:

    • 头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。
    • 有了头结点后,对在第一个元素结点前插入结点和删除第一个结点,其操作与对其它结点的操作统一了。
    • 首元结点也就是第一个元素的结点,它是头结点后边的第一个结点。
    • 头结点不是链表所必需的。

    头指针理解:

    • 在线性表的链式存储结构中,头指针是指链表指向第一个结点的指针,若链表有头结点,则头指针就是指向链表头结点的指针。
    • 头指针具有标识作用,故常用头指针冠以链表的名字。
    • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素

    没有头结点的图示:

    下面分别以头插法和尾插法来创建链表:

    头插法:注:这里第一个创建的链表不带头结点,只含有头指针

    linklist *CreateList_Front()  
    {  
        linklist *head, *p;  
        char ch;  
      
        head = NULL;  
        printf("依次输入字符数据(‘#’表示输入结束):
    ");  
        ch = getchar();  
        while(ch != '#')  
        {  
            p = (linklist*)malloc(sizeof(linklist));  
            p->data = ch;  
            p->next = head;  
            head = p;  
            ch = getchar();             //头插法算法简单 核心就两句p->next = head;head = p;  
        }  
        return head;  
    }  
    /*  随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
    void CreateListHead(LinkList *L, int n)
    {
        LinkList p;
        int i;
        srand(time(0));                         /* 初始化随机数种子 */
        *L = (LinkList)malloc(sizeof(Node));
        (*L)->next = NULL;                      /*  先建立一个带头结点的单链表 */
        for (i=0; i < n; i++)
        {
            p = (LinkList)malloc(sizeof(Node)); /*  生成新结点 */
            p->data = rand()%100+1;             /*  随机生成100以内的数字 */
            p->next = (*L)->next;
            (*L)->next = p;                        /*  插入到表头 */
        }
    }

    其实理解的关键点在于

    首次调用p->next = (*L)->next;  即p->next = null,然后让(*L)->next = p,如此此次创建的结点就作为了尾结点,链表输出变为了逆序

    尾插法:

    linklist *CreateList_End()  
    {  
        linklist *head, *p, *e;  
        char ch;  
      
        head = NULL;  
        e = NULL;  
        printf("请依次输入字符数据('#'表示输入结束):
    ");  
        ch = getchar();  
        while(ch != '#')  
        {  
            p = (linklist*)malloc(sizeof(linklist));  
            p->data = ch;  
            if(head == NULL)        //先判断输入的是不是第一个节点  
            {  
                head = p;             
            }  
            else  
            {  
                e->next = p;     //e始终指向输入的最后一个节点  
            }  
            e = p;  
            ch = getchar();           
        }  
        if(e != NULL)               //如果链表不为空,则最后节点的下一个节点为空  
        {  
            e->next = NULL;  
        }  
        return head;                //尾插法比头插法复杂一些,程序中要做两次判断,分别是判断第一个节点和最后一个节点的判断。且消耗多一个指针变量e。  
    }  

     尾插法也很好理解,其实就是每次将新的节点插入到链表的后面,利用输入ch是否为字符#来控制链表创建结束,这里实现的尾插法也不带头结点

    下面以带头节点的链表为例具体说明链表的各项操作:增删改查

      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <time.h>
      4 
      5 #define OK 1
      6 #define ERROR 0
      7 #define TRUE 1
      8 #define FALSE 0
      9 #define MAXSIZE 20 /* 存储空间初始分配量 */
     10 
     11 //定义节点结构体
     12 typedef struct Node{
     13     int data;
     14     struct Node* next;
     15 }Node;
     16 typedef struct Node* LinkList;
     17 
     18 //为统一操作起见,这里采用带有头结点的链表
     19 //初始化线性顺序表
     20 int InitList(LinkList* L){
     21     *L = (LinkList)malloc(sizeof(Node));//新建一个节点,然后让L指向这个节点
     22     if(!(*L)){//存储分配失败
     23         return ERROR;
     24     }
     25     (*L)->next = NULL;
     26     return OK;
     27 }
     28 // 单链表的插入操作,实际上核心只有两部操作:待插节点为e,1. e->next = p->next;  2. p->next = e
     29 // 注意有一点要说明,这里的链表带有头结点,初始p指向头结点,而不是首个元素节点,这样下面的代码才合理
     30 // 在链表L的第i个位置之前插入新的元素e,并将L的长度加1
     31 int ListInsert(LinkList* L, int i, int e){
     32 
     33     LinkList p, s;
     34     p = *L;
     35     int j = 1;
     36     while(p && j < i){
     37         p = p->next;
     38         j++;
     39     }
     40     if(!p || j > i){
     41         return ERROR;
     42     }
     43     s = (LinkList)malloc(sizeof(Node));
     44     s->data = e;
     45     s->next = p->next;
     46     p->next = s;
     47     return OK;
     48 }
     49 
     50 /**
     51 * 删除链表中的第i个元素,这里就涉及到链表是否带有头结点的情况不同来分析
     52 */
     53 int ListDelete(LinkList* L, int i, int *e){
     54 
     55     LinkList p, q;
     56     p = *L;
     57     int j = 1;
     58     //若带有头结点
     59     while(p->next && j < i){
     60         p = p->next;
     61         j++;
     62     }
     63     if(!(p->next) || j > i){
     64         return ERROR;
     65     }
     66     q = p->next;
     67     p->next = q->next;
     68     *e = q->data;
     69     free(q);
     70     return OK;
     71 }
     72 
     73 
     74 /* 初始条件:顺序线性表L已存在 */
     75 /* 操作结果:依次对L的每个数据元素输出 */
     76 int ListTraverse(LinkList L)
     77 {
     78     LinkList p = L->next;
     79     while(p)
     80     {
     81         visit(p->data);
     82         p=p->next;
     83     }
     84     printf("
    ");
     85     return OK;
     86 }
     87 
     88 
     89 int visit(int e){
     90 
     91     printf("-> %d ", e);
     92     return OK;
     93 }
     94 
     95 int main()
     96 {
     97     LinkList L;
     98     int i, j, k;
     99     int e;
    100 
    101     i = InitList(&L);
    102     srand((unsigned)time(NULL));
    103     for(j = 1; j <= 10; j++){
    104         i = ListInsert(&L, 1, rand()%100);
    105     }
    106     ListTraverse(L);
    107 
    108     ListDelete(&L, 4, &e);//删除从首个元素节点算起的第4个节点
    109     ListTraverse(L);
    110     //printf("Hello world!
    ");
    111     return 0;
    112 }

    运行结果:从运行结果可以看出链表中第4个节点65被删除

     

    从上面的代码可以分析出,单链表的删除操作的时间复杂度是O(N),它必须从链表的头部开始依次遍历,而相对线性表的顺序存储结构而言没有什么优势;但是对于插入元素而言,如果需要在链表中的某个位置插入多个元素,对于数组就需要将插入位置后面的元素全部向后移动,而线性链表则不需要,性能会更好

    所以对于插入删除操作比较频繁的情况优先选择链表

    下面来实现获取指定位置的元素:

     1 //获取L中第i个数据元素的值,并用e返回
     2 int getElement(LinkList* L, int i, int* e){
     3 
     4     LinkList p;
     5     p = L->next;//这里依然是带有头节点的链表,让p指向第一个非头结点元素,也即首元节点;这里保持p和首元节点一致
     6     j = 1;
     7     while(p && j < i){
     8         p = p->next;
     9         j++;
    10     }
    11     if(!p || j >i){
    12         return ERROR;
    13     }
    14     *e = p->data;
    15     return OK;
    16 }

    实际上就是从首元节点开始遍历列表,当找到第i个元素后赋值返回

     置空列表:

     1 //置空列表
     2 int ClearList(LinkList* L){
     3     LinkList p, q;
     4     p = (*L)->next;
     5 
     6     while(p){
     7         q = p->next;
     8         free(p);
     9         p = q;
    10     }
    11     (*L)->next = NULL;
    12     return OK;
    13 }

    实际上核心代码就是7~9行,从首元节点开始依次释放每一个节点,最后将头结点的next指向NULL

     求单链表的倒数第n个数:

    这个思路很简单:只要使用两个指针一前一后,之间间隔元素为n,那么当前面的指针到达链尾时,后面的指针自然就是倒数第n个元素,可以让前面的指针先走n步,然后让两指针同时移动:

     1 // 获取单链表倒数第N个结点值
     2 Status GetNthNodeFromBack(LinkList L, int n, ElemType *e)
     3 {
     4     int i = 0;
     5     LinkList firstNode = L;
     6     while (i < n && firstNode->next != NULL)
     7     {
     8         //正数N个节点,firstNode指向正的第N个节点
     9         i++;
    10         firstNode = firstNode->next;
    11         printf("%d
    ", i);
    12     }
    13     if (firstNode->next == NULL && i < n - 1)
    14     {
    15         //当节点数量少于N个时,返回NULL
    16         printf("超出链表长度
    ");
    17         return ERROR;
    18     }
    19     LinkList secNode = L;
    20     while (firstNode != NULL)
    21     {
    22         //查找倒数第N个元素
    23         secNode = secNode->next;
    24         firstNode = firstNode->next;
    25         //printf("secNode:%d
    ", secNode->data);
    26         //printf("firstNode:%d
    ", firstNode->data);
    27     }
    28     *e = secNode->data;
    29     return OK;
    30 }

     这里来看一个算法题:如何快速找到未知长度单链表的中间结点

    这里采用标尺法:
    思路也很简单,采用两个指针,一个指针是另一个的步进数的二倍,这样当快指针 到达链尾,慢指针就刚好到达链表中间

     1 int GetMiddleElem(LinkList* L, int* e){
     2 
     3     LinkList fast, slow;
     4     slow = fast = L;
     5     while(fast->next != NULL){
     6         if(fast->next->next != NULL){
     7             //fast每次走两步,slow每次走一步 
     8             //不算头结点,从首元节点开始,链表节点为奇数则刚好,若为偶数,则最后一步fast只走一步,slow不走即可
     9             fast = fast->next->next;
    10             slow = slow->next;
    11         }else{
    12             fast = fast->next;
    13         }
    14         //这里当链表长度为偶数时,取得中间元素为前面的,如1、2、3、4、5、6,则取到中间元素为3
    15     }
    16     *e = slow->data;
    17     return OK;
    18 }

     下面分析单链表是否有环的情况:
    有环的定义:

    链表的尾节点指向了链接中间的某个节点。比如下图,如果单链表有环,则在遍历时,在通过结点J之后,会重新回到结点D

    思路依然很简单,就是采用快慢指针,这样有环的话一定会在某一点相遇:这也是最简单的思路

     1 int HasLoop(LinkList* L){
     2     int step1 = 1;
     3     int step2 = 2;
     4 
     5     LinkList p, q;
     6     p = q = L;//这里有无头结点都没有关系
     7 
     8     while(p != NULL &&q!= NULL && q->next != NULL){
     9         p = p->next;
    10         if(q->next != NULL){
    11             q = q->next->next;
    12         }
    13         if(q == p){
    14             return 1;
    15         }
    16     }
    17     return 0;
    18 }

     分析到这里,下面要着重分析一下二级指针的问题:

    //定义节点结构体
     12 typedef struct Node{
     13     int data;
     14     struct Node* next;
     15 }Node;
     16 typedef struct Node* LinkList;
    由上面节点结构体的定义可以看出:Node是一个节点,包含数据和指针,LinkList代表指向Node数据类型的指针变量
    所以下面的定义是一样的:
    Node ** p 和 LinkList * p
    初始化链表:
    //void init_linkedlist(LinkedList *list) {  
    void init_linkedlist(LinkedList *list) {  
        *list = (LinkedList)malloc(sizeof(Node));  
        (*list)->next = NULL;  
    }  

    形参list是一个指向LinkedList数据类型的指针变量,其实也是指向Node数据类型的指针的指针

    main调用时:

    1. LinkedList list, p;  
    2.     init_linkedlist(&list); 
    你会发现传入的是list地址,为何呢?下面来一步步分析:
    1. LinkedList list  
    2. init_linkedlist(&list); 
    3. void init_linkedlist(LinkedList *L)
    4. *L = (LinkedList)malloc(sizeof(Node)); 
     

    首先是main函数中定义一个Node类型的指针,这个指针用list表示,C语言在定义指针的时候也会分配一块内存,一般会占用2个字节或4个字节,现在在大部分的编译器中占用4个字节,这里用4个字节算。在这4个字节的内存中,没有任何值,所以这个指针不指向任何值。也就是说内存中在定义list这个指向Node的指针变量时就会默认分配一块区域,可以理解为这块区域的名字叫list,而内存块中没有任何值

    接下来假设上面list分配的内存地址为0x1000,这里 init_linkedlist(&list); 传入的是list的地址,也即0x1000,注意这里是作为形参传入的,在init_linkedlist函数中,编译器首先为形参list重新分配一个临时指针内存块,这块内存在该函数执行完之后会回收掉,这里将&list赋值给L,也就是说L这个变量的值为0x1000,也即编译器为L分配的内存块中存放的就是0x1000,这样由于L内存块中存放的是main函数中list的地址,所以L指向list,然后新建节点分配的内存块赋值给*L,也就是说list指向该新分配的内存块,也即list变量的值就是新分配节点内存块的地址,也即list内存中的值;于是这样在函数init_linkedlist中分配的一段内存也就能在main函数中反映出来了,main函数中list代表的内存块的就指向了新分配的内存,链表初始化完成

     最后再讲一个关于删除重复元素的题目:
    思路:其实就是采用双重循环,外层循环每遍历一个节点,内层循环就从该节点开始遍历剩下所有节点,并与这个节点比较,若相同就删除

     1 LinkList RemoveDupNode(LinkList L)//删除重复结点的算法
     2 {
     3     LinkList p,q,r;
     4     p=L->next;
     5     while(p)    // p用于遍历链表
     6     {
     7          q=p;
     8          while(q->next) // q遍历p后面的结点,并与p数值比较
     9          {
    10              if(q->next->data==p->data)
    11              {
    12                  r=q->next; // r保存需要删掉的结点
    13                  q->next=r->next;   // 需要删掉的结点的前后结点相接
    14                  free(r);
    15              }
    16              else
    17                  q=q->next;
    18          }
    19          p=p->next;
    20     }
    21     return L;
    22 }

     注:这里附上一个链表的题目:给定一个链表,不知道头结点,只知道所要删除的节点指针,如何删除该节点?

    分析,由于不知道链表的头结点,所以也就没法直接遍历,这里也就没法采用快慢指针的方式定位到删除节点的前向节点,该方法行不通

    第二种思路采用删除节点后面的节点依次前移覆盖掉前面一个节点,以赋值的方式将待删除节点给覆盖掉,实现删除的效果;问题:如果待删除节点为尾节点,即链表的最后一个节点,遇到的问题是没法置空,因为待删除节点的前向节点没法获取到,则它的指针域也就没法改变

  • 相关阅读:
    装饰复杂函数
    装饰器01
    闭包
    函数的嵌套定义
    名称空间
    函数的嵌套调用
    函数的对象
    形参
    实参
    形参与实参
  • 原文地址:https://www.cnblogs.com/CoolRandy/p/4320990.html
Copyright © 2011-2022 走看看