编写程序,建立一个带有节点的单向链表,输入字符串,并按从小到大顺序组织到链表中

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写程序,建立一个带有节点的单向链表,输入字符串,并按从小到大顺序组织到链表中相关的知识,希望对你有一定的参考价值。

int main()

Link head; //链表(不带头节点)

int n;

printf("输入链表的长度n: ");

scanf("%d",&n);

printf("连续输入%d个数据(以空格隔开): ",n);

head=CreateLink(n);

printf("\\n原本链表的节点是: ");

DispLink(head);

LinkSort(head);

printf("\\n从大到小排序之后: ");

DispLink(head);

printf("\\n");

return 0;

链表的具体存储表示为:

① 用一组任意的存储单元来存放线性表的结点(这组存储单元既可以是连续的,也可以是不连续的)

② 链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息(称为指针(pointer)或链(link))

链式存储是最常用的存储方式之一,它不仅可用来表示线性表,而且可用来表示各种非线性的数据结构。

以上内容参考:百度百科-单链表

参考技术A 链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。

通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:

1. 单链表

图1 单链表

单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。

2. 双链表

图2 双链表

通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。

3. 循环链表

循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。

--------------------------------------------------------------------------------
回页首
二、 Linux 2.6内核链表数据结构的实现

尽管这里使用2.6内核作为讲解的基础,但实际上2.4内核中的链表结构和2.6并没有什么区别。不同之处在于2.6扩充了两种链表数据结构:链表的读拷贝更新(rcu)和HASH链表(hlist)。这两种扩展都是基于最基本的list结构,因此,本文主要介绍基本链表结构,然后再简要介绍一下rcu和hlist。

链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件):

struct list_head
struct list_head *next, *prev;
;

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。

和第一节介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):

struct list_node
struct list_node *next;
ElemType data;
;

因为ElemType的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的C++程序员应该知道,标准模板库中的<list>采用的是C++ Template,利用模板抽象出和数据项类型无关的链表操作接口。

在Linux内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员,例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_ops结构来描述Netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个(struct list_head list)成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在[net/core/netfilter.c]中的nf_sockopts(struct list_head)。从下图中我们可以看到,这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。Linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分。

图3 nf_sockopts链表示意图

--------------------------------------------------------------------------------
回页首
三、 链表操作接口

1. 声明和初始化

实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

#define LIST_HEAD_INIT(name) &(name), &(name)
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

static inline int list_empty(const struct list_head *head)

return head->next == head;


除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表:

#define INIT_LIST_HEAD(ptr) do \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
while (0)

我们用INIT_LIST_HEAD(&nf_sockopts)来使用它。

2. 插入/删除/合并

a) 插入

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);

因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用

__list_add(new, head, head->next);



__list_add(new, head->prev, head);

来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:

list_add(&new_sockopt.list, &nf_sockopts);

从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。

b) 删除

static inline void list_del(struct list_head *entry);

当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:

list_del(&new_sockopt.list);

被剔除下来的new_sockopt.list,prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。

c) 搬移

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);

例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。

d) 合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):

图4 链表合并list_splice(&list1,&list2)

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

static inline void list_splice_init(struct list_head *list, struct list_head *head);

该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

3. 遍历

遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

a) 由链表节点到数据项变量

我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用:

list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

这里"list"正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。

list_entry的使用相当简单,相比之下,它的实现则有一些难懂:

#define list_entry(ptr, type, member) container_of(ptr, type, member)
container_of宏定义在[include/linux/kernel.h]中:
#define container_of(ptr, type, member) ( \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );)
offsetof宏定义在[include/linux/stddef.h]中:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

size_t最终定义为unsigned int(i386)。

这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

container_of()和offsetof()并不仅用于链表操作,这里最有趣的地方是((type *)0)->member,它将0地址强制"转换"为type结构的指针,再访问到type结构中的member成员。在container_of宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;在offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。

如果这么说还不好理解的话,不妨看看下面这张图:

图5 offsetof()宏的原理

对于给定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

b) 遍历宏

在[net/core/netfilter.c]的nf_register_sockopt()函数中有这么一段话:

……
struct list_head *i;
……
list_for_each(i, &nf_sockopts)
struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;
……

……

函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在[include/linux/list.h]中,list_for_each()宏是这么定义的:

#define list_for_each(pos, head) \
for (pos = (head)->next, prefetch(pos->next); pos != (head); \
pos = pos->next, prefetch(pos->next))

它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。

那么在nf_register_sockopt()中实际上就是遍历nf_sockopts链表。为什么能直接将获得的list_head成员变量地址当成struct nf_sockopt_ops数据项变量的地址呢?我们注意到在struct nf_sockopt_ops结构中,list是其中的第一项成员,因此,它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是:

struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);

大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member) ……

与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。nf_register_sockopt()函数可以利用这个宏而设计得更简单:

……
struct nf_sockopt_ops *ops;
list_for_each_entry(ops,&nf_sockopts,list)
……

……

某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。

如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以使用list_for_each_entry_continue(pos,head,member)。有时还会出现这种需求,即经过一系列计算后,如果pos有值,则从pos开始遍历,如果没有,则从链表头开始,为此,Linux专门提供了一个list_prepare_entry(pos,head,member)宏,将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

4. 安全性考虑

在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面:

a) list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。

b) 遍历时节点删除

前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

--------------------------------------------------------------------------------
回页首
四、 扩展

1. hlist

图6 list和hlist

精益求精的Linux链表设计者(因为list.h没有署名,所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表对于HASH表来说"过于浪费",因而另行设计了一套用于HASH表应用的hlist数据结构--单指针表头双循环链表,从上图可以看出,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗。

因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间,以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述。为此,hlist节点的prev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的"*(node->pprev)"访问和修改前驱节点的next(或first)指针。

2. read-copy update

在Linux链表功能接口中还有一系列以"_rcu"结尾的宏,与以上介绍的很多函数一一对应。RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能。

我们知道,系统中数据读取操作远多于写操作,而rwlock机制在smp环境下随着处理机增多性能会迅速下降(见参考资料4)。针对这一应用背景,IBM Linux技术中心的Paul E. McKenney提出了"读拷贝更新"的技术,并将其应用于Linux内核中。RCU技术的核心是写操作分为写-更新两步,允许读操作在任何时候无阻访问,当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很小一部分,对于RCU的实现分析已超出了本文所及,有兴趣的读者可以自行参阅本文的参考资料;而对RCU链表的使用和基本链表的使用方法基本相同。追问

那这道题怎么做呢?答案是啥?多谢

本回答被提问者采纳

单向链表

  单向链表每个节点由两个成员组成:一个是数据域,一个是指向自身结构的指针类型成员。如:

    struct slist

    {

      int data;

      struct slist *next;

    };

    typedef struct slist SLIST;

  单向链表的基本算法包括:链表的建立、节点数据域的输出、节点的插入和删除。

  (1) 建立带有头结点的单向链表

  建立单向链表的主要步骤如下:

  1. 读取数据
  2. 生成新节点
  3. 将数据存入节点的成员变量中
  4. 将新节点插入到链表中,重复上述操作直至输入结束。

  例1 编写函数 creat_slist,建立带有头结点的单向链表。节点数据域中的数值从键盘输入,以 -1 作为输入结束标志。链表头结点的地址由函数值返回。

program

技术分享
 1 //建立带有头结点的单向链表
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 
 5 //声明结构体类型 SLIST
 6 struct slist
 7 {
 8     int data;
 9     struct slist *next;
10 };
11 typedef struct slist SLIST;
12 
13 //声明链表建立函数
14 SLIST* creat_slist();
15 void print_slist(SLIST* head);
16 void insert_snode(SLIST* head, int x, int y);
17 
18 int main()
19 {
20     SLIST *head;
21     head = creat_slist();    //调用链表建立函数,得到头结点地址
22     print_slist(head);
23     return 0;
24 }
25 
26 //定义链表建立函数
27 SLIST* creat_slist()
28 {    
29     //s指向新生成的节点;r指向链表的尾节点
30     SLIST *h, *s, *r;
31     int c;
32 
33     h = (SLIST*)malloc(sizeof(SLIST));
34     r = h;
35     scanf("%d", &c);        //读入数据
36     
37     while (c != -1)            //-1作为输入结束标志
38     {
39         s = (SLIST*)malloc(sizeof(SLIST));        //生成新节点
40         s->data = c;        //将读入的数据存入新节点的数据域
41         r->next = s;        //新节点链接到表尾
42         r = s;                //r指向当前表尾
43         scanf("%d", &c);    //读入数据
44     }
45     r->next = \\0;            //链表结束标志
46     return h;
47 }
48 
49 //顺序输出链表各节点数据
50 void print_slist(SLIST* head)
51 {
52     SLIST* p;
53     p = head->next;            //p指向头结点后的第一个节点
54     
55     if (p == \\0)
56     {
57         printf("Linklist is null !\\n");    //链表为空(只有头结点)
58     }
59     else
60     {
61         printf("head");
62         do
63         {
64             printf("->%d", p->data);    //输出当前节点数据域中的值
65             p = p->next;                //p指向下一节点
66         } while (p != \\0);            //未到链表尾,循环继续
67     }
68 }
69 
70 //在值为x的节点前插入值为y的节点
71 void insert_snode(SLIST* head, int x, int y)
72 {
73     SLIST *s, *p, *q;
74     s = (SLIST*)malloc(sizeof(SLIST));    //生成新节点
75     s->data = y;                        //在新节点中存入y值
76     q = head;
77     p = head->next;                        //工作指针初始化,p指向第一个节点
78     while ( (p != \\0) && (p->data != x) )
79     {
80         q = p;
81         p = p->next;                    //q指向p的前趋节点
82     }
83     //x存在,插在x之前;x不存在,p的值为NULL,插在表尾
84     s->next = p;
85     q->next = s;
86 }
View Code

  技术分享

  我们在函数中定义了一个名为 h 的指针变量,用于存放头结点的地址;另外还定义了两个工作指针:s 和 r,其中指针 s 用来指向新生成的节点,指针 r 总是指向当前链表的尾节点。每当把 s 所指的新开辟的节点连接到表尾后,r 便移向这一新的表尾节点,这时再用 s 去指向下一个新开辟的节点,就是使 r 承上,用 s 启下。

  链表最后一个节点的指针域中置 ‘\\0‘(NULL),作为单向链表的结束标志。链表建成后,头结点的地址作为 creat_slist 函数的返回值由 return 语句返回并赋给 main 函数中的指针变量 head,因此函数的类型应该是基类型为 SLIST 的指针类型。

  调用 creat_slist1 函数时,若一开始就输入-1,控制流程不会进入 while 循环,而直接执行循环之后的 r->next = ‘\\0‘; 语句。这时建立的是一个空链表,其结构如图1所示。由此可见,判断此链表是否为空链表,可用条件:h->next == ‘\\0‘;

技术分享

图1 带有头结点的空链表

  (2) 顺序访问链表中各节点的数据域

  所谓“访问”,可以理解为取各节点的数据域中的值进行各种运算、修改各节点的数据域中的值等一系列的操作。

  输出单向链表各节点数据域中的内容的算法比较简单,只需利用一个工作指针 p ,从头到尾依次指向链表中的每个节点;当指针指向某个节点时,就输出该节点数据域中的内容,直到遇到链表结束标志为止。如果是空链表,就只输出相关信息并返回调用函数。

  例2 编写函数 print_slist,顺序输出单向链表各节点数据域中的内容。

program

技术分享
 1 //建立带有头结点的单向链表
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 
 5 //声明结构体类型 SLIST
 6 struct slist
 7 {
 8     int data;
 9     struct slist *next;
10 };
11 typedef struct slist SLIST;
12 
13 //声明链表建立函数
14 SLIST* creat_slist();
15 void print_slist(SLIST* head);
16 void insert_snode(SLIST* head, int x, int y);
17 
18 int main()
19 {
20     SLIST *head;
21     head = creat_slist();    //调用链表建立函数,得到头结点地址
22     print_slist(head);
23     return 0;
24 }
25 
26 //定义链表建立函数
27 SLIST* creat_slist()
28 {    
29     //s指向新生成的节点;r指向链表的尾节点
30     SLIST *h, *s, *r;
31     int c;
32 
33     h = (SLIST*)malloc(sizeof(SLIST));
34     r = h;
35     scanf("%d", &c);        //读入数据
36     
37     while (c != -1)            //-1作为输入结束标志
38     {
39         s = (SLIST*)malloc(sizeof(SLIST));        //生成新节点
40         s->data = c;        //将读入的数据存入新节点的数据域
41         r->next = s;        //新节点链接到表尾
42         r = s;                //r指向当前表尾
43         scanf("%d", &c);    //读入数据
44     }
45     r->next = \\0;            //链表结束标志
46     return h;
47 }
48 
49 //顺序输出链表各节点数据
50 void print_slist(SLIST* head)
51 {
52     SLIST* p;
53     p = head->next;            //p指向头结点后的第一个节点
54     
55     if (p == \\0)
56     {
57         printf("Linklist is null !\\n");    //链表为空(只有头结点)
58     }
59     else
60     {
61         printf("head");
62         do
63         {
64             printf("->%d", p->data);    //输出当前节点数据域中的值
65             p = p->next;                //p指向下一节点
66         } while (p != \\0);            //未到链表尾,循环继续
67     }
68 }
69 
70 //在值为x的节点前插入值为y的节点
71 void insert_snode(SLIST* head, int x, int y)
72 {
73     SLIST *s, *p, *q;
74     s = (SLIST*)malloc(sizeof(SLIST));    //生成新节点
75     s->data = y;                        //在新节点中存入y值
76     q = head;
77     p = head->next;                        //工作指针初始化,p指向第一个节点
78     while ( (p != \\0) && (p->data != x) )
79     {
80         q = p;
81         p = p->next;                    //q指向p的前趋节点
82     }
83     //x存在,插在x之前;x不存在,p的值为NULL,插在表尾
84     s->next = p;
85     q->next = s;
86 }
View Code

  技术分享

  (3)在单向链表中插入节点

  在单向链表中插入节点,首先要确定插入的位置。当待插节点插在指针 p 所指的节点之前称为“前插”;当待插节点插在指针 p 所指节点之后称为“后插”。图2示意了“前插”操作过程中各指针的指向。

技术分享

图2 单向链表中节点的插入

  当进行“前插”操作时,需要三个工作指针:图中s 用来指向新开辟的节点;用 p 指向插入的位置;q 指向 p 的前趋节点(由于是单向链表,没有指针 q,就无法通过 p 去指向它前趋节点)。

  例3 编写函数 insert_snode,它的功能是:在值为 x 的节点前插入值为 y 的节点,若值为 x 的节点不存在,则插在表尾。

program

技术分享
 1 //建立带有头结点的单向链表
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 
 5 //声明结构体类型 SLIST
 6 struct slist
 7 {
 8     int data;
 9     struct slist *next;
10 };
11 typedef struct slist SLIST;
12 
13 //声明链表建立函数
14 SLIST* creat_slist();
15 void print_slist(SLIST* head);
16 void insert_snode(SLIST* head, int x, int y);
17 
18 int main()
19 {
20     SLIST *head;
21     head = creat_slist();    //调用链表建立函数,得到头结点地址
22     print_slist(head);
23     return 0;
24 }
25 
26 //定义链表建立函数
27 SLIST* creat_slist()
28 {    
29     //s指向新生成的节点;r指向链表的尾节点
30     SLIST *h, *s, *r;
31     int c;
32 
33     h = (SLIST*)malloc(sizeof(SLIST));
34     r = h;
35     scanf("%d", &c);        //读入数据
36     
37     while (c != -1)            //-1作为输入结束标志
38     {
39         s = (SLIST*)malloc(sizeof(SLIST));        //生成新节点
40         s->data = c;        //将读入的数据存入新节点的数据域
41         r->next = s;        //新节点链接到表尾
42         r = s;                //r指向当前表尾
43         scanf("%d", &c);    //读入数据
44     }
45     r->next = \\0;            //链表结束标志
46     return h;
47 }
48 
49 //顺序输出链表各节点数据
50 void print_slist(SLIST* head)
51 {
52     SLIST* p;
53     p = head->next;            //p指向头结点后的第一个节点
54     
55     if (p == \\0)
56     {
57         printf("Linklist is null !\\n");    //链表为空(只有头结点)
58     }
59     else
60     {
61         printf("head");
62         do
63         {
64             printf("->%d", p->data);    //输出当前节点数据域中的值
65             p = p->next;                //p指向下一节点
66         } while (p != \\0);            //未到链表尾,循环继续
67     }
68 }
69 
70 //在值为x的节点前插入值为y的节点
71 void insert_snode(SLIST* head, int x, int y)
72 {
73     SLIST *s, *p, *q;
74     s = (SLIST*)malloc(sizeof(SLIST));    //生成新节点
75     s->data = y;                        //在新节点中存入y值
76     q = head;
77     p = head->next;                        //工作指针初始化,p指向第一个节点
78     while ( (p != \\0) && (p->data != x) )
79     {
80         q = p;
81         p = p->next;                    //q指向p的前趋节点
82     }
83     //x存在,插在x之前;x不存在,p的值为NULL,插在表尾
84     s->next = p;
85     q->next = s;
86 }
View Code

  由于本例中的单向链表采用了带有头结点的结构,不需要单独处理新节点插在表头的情况,从而简化了操作。函数 insert_snode 中综合运用了“查找”和“前插”的算法。在进行插入操作的过程中,可能遇到三种情况,函数 insert_snode 将对这三种情况进行处理:

  (1)链表非空,值为 x 的节点存在,新节点应插在该节点之前。

  (2)链表非空,但值为 x 的节点不存在,新节点应插在表尾。

  (3)链表为空表,这种情况相当于 x 的节点不存在,新节点应插在表尾,即插在头结点之后,作为表的第一个节点。

  在函数中,对于空表,在执行 p = head->next; 后,p 的值就为 NULL ,因此不会进入循环;当链表非空时进入循环,在 while 循环中,当出现 p == ‘\\0‘ 时,将退出循环,这意味着查找结束,在链表中不存在值为 x 的节点。对于这两种情况,语句 s->next = p; q->next = s; 将新节点插在表尾。当链表中存在值为 x 的节点时,p 的值一定部位 ‘\\0‘,语句 s->next = p; q->next = s; 将新节点插在 x 节点之前。

  需要注意的是:while 循环中两个条件的顺序不能对调。当 p == NULL 时,C 编译程序将“短路”掉第二个条件,不做判断;否则如果先判断 p->data != x 的条件,由于 p 中的值已为 NULL,这时再要访问 p->data, 就会发生访问虚地址的错误操作。

  (4)删除单向链表中的节点

  为了删除单项链表中的某个节点,首先要找到待删除节点的前趋节点,然后将此前趋节点的指针域去指向待删除节点的后续节点(q->next = p->next),最后释放被删除节点所占空间 free(p) 即可。图3示意了节点的删除操作。

技术分享

图3 单向链表节点的删除

以上是关于编写程序,建立一个带有节点的单向链表,输入字符串,并按从小到大顺序组织到链表中的主要内容,如果未能解决你的问题,请参考以下文章

C语言中数据结构中的单向链表的问题;

数据结构编程实验

6-3 建立学生信息链表 (20分) 本题要求实现一个将输入的学生成绩组织成单向链表的简单函数。 函数接口定义: ```cpp void input(); ``` 该函数利用scanf从输入中获

数据结构提高:链表

数据结构提高:链表

什么叫带头结点的链表? 什么叫不带头结点的链表?