引言:用C#把数据结构单链表基本操作部分实现了一遍,并做了个小总结,现发布出来,欢迎各位大虾拍砖!谢谢!
一、链表描述
单链表在现实生活中可以找到例子,如自行车的链条,它由若干个链子组成,每个链子由一个铁圈和一个铁栓组成,链子就好比链表的结点,铁圈就好比链表的数据域,铁栓好比链表的引用域。
链条(链表)
链子(结点)
单链表(LinkedList):
链表是用一组任意的存储单元来存储线性表中的数据元素。这组存储单元可以是连续的,也可以是不连续的。
结点(Node):
数据域,存储数据元素;
引用域,存储与它相邻的数据元素的存储地址信息。
线性表通过每个结点的引用域形成了一根“链条”
二、结点类和链表类定义
(说明:为了演示的简便,结点的数据域定义为int型了,大家可以改为其它数据类型或者扩展为泛型)
1、单链表结点类定义:
2 /// 单链表节点类
3 /// </summary>
4 public class Node
5 {
6 public Node()
7 {
8 data = 0;
9 next = null;
10 }
11 public Node(int val,Node p)
12 {
13 data = val;
14 next = p;
15 }
16 public Node(int val)
17 {
18 data = val;
19 next = null;
20 }
21 public Node(Node p)
22 {
23 data = 0;
24 next = p;
25 }
26
27 private int data;
28 private Node next;
29 public int Data
30 {
31 get
32 {
33 return data;
34 }
35 set
36 {
37 data = value;
38 }
39 }
40 public Node Next
41 {
42 get
43 {
44 return next;
45 }
46 set
47 {
48 next = value;
49 }
50 }
51 }
2、单链表类定义:
2 /// 单链表类
3 /// </summary>
4 public class LinkedList
5 {
6 public LinkedList()
7 {
8 head = null;
9 }
10 private Node head;
11 public Node Head
12 {
13 get
14 {
15 return head;
16 }
17 set
18 {
19 head = value;
20 }
21 }
22 //创建操作(头插和尾插两种)
23 //查找操作(按值和按序号两种)
24 //插入操作(结点前插入、结点后插入和指定位置插入)
25 //删除操作(按值和按序号两种)
26 //遍历操作
27 //反转操作
28 //求长度操作
29 }
三、链表基本操作算法
链表的基本操作包括七大操作,创建操作(头插和尾插两种)、查找操作(按值和按序号两种)、插入操作(结点前插入、结点后插入和指定位置插入)、删除操作(按值和按序号两种)、遍历操作、反转操作、求长度操作。
1、创建操作
1.1头插法创建链表
头插法是在链表的头部插入结点建立单链表,建立单链表从空表开始,每读入一个数据元素就申请一个结点,然后插在链表的头部。
2 public void CreateListHead()
3 {
4 int d = Int32.Parse(Console.ReadLine());
5 while (d != -1)
6 {
7 Node p = new Node(d);
8 p.Next = head;
9 head = p;
10 d = Int32.Parse(Console.ReadLine());
11 }
12 }
1.2 尾插法创建链表
头部插入结点建立单链表读入的数据元素的顺序与生成的链表中元素的顺序是相反的。若希望次序一致,则用尾部插入的方法。因为每次是将新结点插入到链表的尾部,所以需加入一个引用P用来始终指向链表中的尾结点,以便能够将新结点插入到链表的尾部 。
2 /// 创建操作(尾插法创建链表)
3 /// </summary>
4 public void CreateListTail()
5 {
6 Node p = head;
7 int d = Int32.Parse(Console.ReadLine());
8 while(d!=-1)
9 {
10 Node s = new Node(d);
11 if (head == null)
12 {
13 head = s;
14 }
15 else
16 {
17 p.Next = s;
18 }
19 p = s;
20 d = Int32.Parse(Console.ReadLine());
21 }
22 }
2.查找操作
2.1 按值查找链表
单链表中的按值查找是指在表中查找其值满足给定值的结点,并返回该结点。
思路:通过引用P遍历,直到P没有到达表尾之后,如果当前结点数据与关键字相符,返回该结点,否则p向后移动,准备查找下一个结点,如果所有结点都不匹配,则返回null。
等价精简代码的思路是当循环满足p没有到达表尾之后和当前结点数据不等于关键字两个条件时,p向后移动,如果找到关键字,循环终止,返回p指向的当前结点,即返回p;如果一直找不到,即p已近移动到了表尾之后,p的值为null了,返回p也就是返回null。
2 /// 查找操作(按值查找链表)
3 /// </summary>
4 /// <param name="key">待查找节点的值</param>
5 /// <returns>节点</returns>
6 public Node GetNodeByVal(int key)
7 {
8 Node p = head;
9 while (p != null)
10 {
11 if (p.Data == key)
12 {
13 return p;
14 }
15 p = p.Next;
16 }
17 return null;
18 //---以下为等价精简代码---
19 //Node p = head;
20 //while (p != null && p.Data != key)
21 //{
22 // p = p.Next;
23 //}
24 //return p;
25 }
2.2 按序号查找链表
通过指定的序号查找链表中对应的结点。
思路和按值查找类似。
2 /// 查找操作(按序号查找链表)
3 /// </summary>
4 /// <param name="i">待查找节点序号</param>
5 /// <returns>节点</returns>
6 public Node GetNodeByIndex(int i)
7 {
8 Node p = head;
9 int j = 1;
10 while (p != null)
11 {
12 if (j == i)
13 {
14 return p;
15 }
16 j++;
17 p = p.Next;
18 }
19 return null;
20 //---以下为等价精简代码---
21 //Node p = head;
22 //int j = 1;
23 //while (p != null && j < i)
24 //{
25 // p = p.Next;
26 // j++;
27 //}
28 //return p;
29 }
3.插入操作
3.1指定节点之后插入新结点
给定一个关键结点的值,在该结点的后面插入一个新结点。
思路:通过新结点的值构造新结点,通过关键字的值找到关键结点的引用;如果找不到关键结点或者空表的话给出提示,否则执行新结点的插入操作,插入操作是先连(新结点先连到关键结点的后继结点,s.Next = q.Next;),后断(断开关键结点和原后继结点的连接,使其连到新结点,q.next=s),
2 /// 插入操作(指定节点之后插入新节点)
3 /// </summary>
4 /// <param name="key">指定节点的值</param>
5 /// <param name="x">新节点的值</param>
6 public void InsertAfter(int key, int x)
7 {
8 Node s = new Node(x);
9 Node q = GetNodeByVal(key);
10 if (head == null || q == null)
11 {
12 Console.WriteLine("The linked list is empty or position is error!");
13 }
14 else
15 {
16 s.Next = q.Next;
17 q.Next = s;
18 }
19 }
3.2 指定结点之前插入新结点
给定一个关键结点的值,在该结点的前面插入一个新结点。
思路:要插入一个新结点,需要先找前继结点,这样操作就和上面的后插结点类似了。可是这里给的是后继结点,怎么办,我们需要定义一个引用p从表头不断后移直到指向关键结点的前继结点,即通过p.next!=q循环判断p的后继结点是否是关键结点,如果不是,p继续后移,直到找到关键结点,此时p结点就是关键结点的前继结点,也就是相当于在p之后插入新结点了,然后先连后断,这里有一个特殊情况得单独处理,就是当关键结点是第一个结点时,因为此时关键结点没有前继结点。
2 /// 插入操作(指定节点之前插入新节点)
3 /// </summary>
4 /// <param name="key">指定节点的值</param>
5 /// <param name="x">新节点的值</param>
6 public void InsertBefore(int key, int x)
7 {
8 Node s = new Node(x);
9 Node p = head;
10 Node q = GetNodeByVal(key);
11
12 if (head == null || q == null)
13 {
14 Console.WriteLine("The linked list is empty or position is error!");
15 }
16 else
17 {
18 if (q == head)
19 {
20 s.Next = head;
21 head = s;
22 }
23 else
24 {
25 while (p.Next != q)
26 {
27 p = p.Next;
28 }
29 s.Next = q;
30 p.Next = s;
31 }
32 }
33 }
3.3 指定位置插入新结点
在链表的某个位置插入新的结点。
思路:依旧是通过先找前继结点入手,这里需要定义两个引用p(不断后移定位关键结点)和q(保存后继结点),新节点插在结点i-1和结点i之间,这里我们通过找结点i-1来找其后继结点i,这样即使i=表长+1也就是新结点插在表尾之后的话也适用,此时结点i-1就是最后一个结点,结点i就是null,然后先连后断,但如果我们通过找结点i来找其前继结点i-1的话,结点i最多只能是表尾结点,无法在表尾之后插入新结点。同上,这里如果插入位置是1的话也需要特殊处理,因为该位置没有前继结点,如果找不到该位置即i大于长度+1情况下,给出相应提示,空表和i参数不合法也需给出提示。
2 /// 插入操作(指定位置插入新节点)
3 /// </summary>
4 /// <param name="i">指定位置序号</param>
5 /// <param name="x">新节点的值</param>
6 public void InsertIndex(int i, int x)
7 {
8 if (head == null || i < 1)
9 {
10 Console.WriteLine("The linked list is empty or position is error!");
11 }
12 else
13 {
14 Node s = new Node(x);
15 Node p = head;
16 Node q = new Node();
17 int j = 1;
18 if (i == 1)
19 {
20 s.Next = head;
21 head = s;
22 }
23 else
24 {
25 while (p != null)
26 {
27 if (j == i - 1)
28 {
29 q = p.Next;
30 s.Next = q;
31 p.Next = s;
32 return;
33 }
34 j++;
35 p = p.Next;
36 }
37 Console.WriteLine("The position is error!");
38 }
39 }
40 }
4.删除操作
4.1 按值删除节点
给定一个关键结点的值,在该值第一次出现的位置删除掉。
思路:这里需要定义两个引用p(不断后移定位关键结点)和q(存储p的前继结点),当关键结点是第一个结点时需要特殊处理,空表或者找不到关键结点需给出提示。循环移动p若找不到该关键结点即也需提示。
2 /// 删除操作(按值删除节点,只能够删第一次出现的节点)
3 /// </summary>
4 /// <param name="key"></param>
5 public void DeleteValue(int key)
6 {
7 if (head == null)
8 {
9 Console.WriteLine("The linked list is empty!");
10 }
11 else
12 {
13 if (head.Data == key)
14 {
15 head = head.Next;
16 }
17 else
18 {
19 Node p = head;
20 Node q = new Node();
21 while (p != null)
22 {
23 if (p.Data == key)
24 {
25 q.Next = p.Next;
26 return;
27 }
28 q = p;
29 p = p.Next;
30 }
31 Console.WriteLine("The Node is not exist!");
32 }
33 }
34 }
4.2按位置删除结点
给定位置序号,删除该序号对应的结点。
思路:同上,这里也需要定义两个引用p(不断后移定位关键结点)和q(存储p的前继结点),当关键结点是第一个结点时需特殊处理,空表以及位置参数不合法时需给出提示,循环移动p若找不到该位置即i>表长时也需提示。
2 /// 删除操作(按位置删除节点)
3 /// </summary>
4 /// <param name="i">指定位置序号</param>
5 public void DeleteIndex(int i)
6 {
7 if (head == null || i < 1)
8 {
9 Console.WriteLine("The linked list is empty or position is error!");
10 }
11 else
12 {
13 if (i == 1)
14 {
15 head = head.Next;
16 }
17 else
18 {
19 Node p = head;
20 Node q = new Node();
21 int j = 1;
22 while (p != null)
23 {
24 if (j == i)
25 {
26 q.Next = p.Next;
27 return;
28 }
29 j++;
30 q = p;
31 p = p.Next;
32 }
33 Console.WriteLine("The Node is not exist!");
34 }
35 }
36 }
5.反转链表
把原来的链表顺序颠倒过来
思路:需要定义两个引用p和newHead,首先p和head均指向表头,head后移,p指向的结点转向指向newHead,newHead再指向p,直到head移动到表尾,此时全部转向完成。
2 /// 反转链表
3 /// </summary>
4 public void ReverseList()
5 {
6 Node p = null;
7 Node newHead = null;
8 while(head!=null)
9 {
10 p = head;
11 head = head.Next;
12 p.Next = newHead;
13 newHead = p;
14 }
15 head = newHead;
16 }
6.遍历链表
从链表第一个结点开始到最后一个结点,依次输出。
思路:需要定义引用pRead不断后移逐个读取结点的值,当pRead到达表尾之后,即为null时循环结束。
2 /// 遍历链表
3 /// </summary>
4 public void ShowList()
5 {
6 Node pRead = head;
7 while (pRead != null)
8 {
9 Console.Write(pRead.Data);
10 pRead = pRead.Next;
11 if (pRead != null)
12 {
13 Console.Write(",");
14 }
15 }
16 Console.WriteLine();
17 }
7.求链表长度
求链表结点的个数,个数就是链表的长度。
思路:需要定义引用p不断后移对计数器累加,当p到达表尾之后,即为null时循环结束。
2 /// 求链表长度
3 /// </summary>
4 /// <returns>链表长度</returns>
5 public int GetLength()
6 {
7 Node p = head;
8 int len = 0;
9 while (p != null)
10 {
11 len++;
12 p = p.Next;
13 }
14 return len;
15 }