单链表的各项常规操作

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

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

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

头指针理解:

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

没有头结点的图示:

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

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

linklist *CreateList_Front()  
{  
    linklist *head, *p;  
    char ch;  
  
    head = NULL;  
    printf("依次输入字符数据(‘#’表示输入结束):\n");  
    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("请依次输入字符数据('#'表示输入结束):\n");  
    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("\n");
 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!\n");
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\n", i);
12     }
13     if (firstNode->next == NULL && i < n - 1)
14     {
15         //当节点数量少于N个时,返回NULL
16         printf("超出链表长度\n");
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\n", secNode->data);
26         //printf("firstNode:%d\n", 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 }

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

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

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

posted @ 2015-03-07 21:54  CoolRandy  阅读(583)  评论(0编辑  收藏  举报