数据结构之单链表(C#版)
简介
数据结构和算法是程序员的内容心法,正好,我也刚刚学完一个系列的数据结构和算法的课程,那就趁热打铁,先给大家分享一下数据结构之单链表,那么什么是单链表呢?
1.我用灵魂画了一下下面这幅图,来大致讲一下什么是链表。
可以看到,里面每个人(节点)都看着下一个人(指向下一个元素或数据),只有最后一个小伙伴(尾节点),看着你,那是因为,他后面没有小伙伴了(没有子节点了),他也不知道该看哪里(尾节点的子节点为空),这个是讲人话版本的链表解释。
2.下面来个程序员版的链表解释。
可以看到,每个节点都保存有自身的数据和对下一个节点的引用,链表有两个比较特殊的节点,一个是头结点,它没有前驱节点(父节点),另外一个是尾节点,它没有后继节点(子节点),图中的箭头,代表的就是节点间的引用,其引用方向是单一的,只能从前一节点引用后一节点,所以称为单链表,那么双链表,就是节点可以互相引用。
与数组的区别
1.内存分布
数组:数组的内存分布是连续的,就是说,数组中第10个元素在内存中的位置,必然是在第0个元素后的第10个位置中,就跟班级里面的座位一样,第10个座位的同学,肯定是在第0个座位的同学往后数10个位置。
链表:链表的内存分布跟数组就完全不一样了喔,链表中每个元素,它都知道下一个元素所在的内存位置(引用),还是拿班级作比喻, 第10个座位的同学,不一定是第0个座位的同学往后数10个位置了,而是第10个座位的同学,告诉了第9个座位的同学说:“喂,我是你后面的同学,我现在的位置是在第2排第3列的位置”,如果要从第0个座位的同学找到第10个座位的同学,那么要怎么做呢?就必须让第0个座位的同学问第1个座位的同学问第2个座位的同学问第3个座位的同学…这里省略一万个同学,然后到了第9个座位的同学了,这个时候,终于知道第10个座位的同学在那里了。
2.数据的获取
数组:因为数组的内存分布的特性,可以直接通过‘获取第N个元素获取’(即内存中从第0个元素的地址中偏移N即可一步获得第N个元素的数据),用班级同学比喻的话就是,第0个同学说:“喂,我后面第N个同学,你给我出来!”,然后第N个同学就自动出来了。
链表:链表就麻烦多了,必须每一个节点去遍历,直到找到需要的数据为止, 用班级同学来比喻的话就是,要找到那个做坏事的同学,那么久要从第0个同学问第1个同学问第2个同学问第3个同学…一直问到那个承认是自己的那个同学为止(假设同学们都很诚实),当然也有可能所有同学都没做坏事,那么就找不到做坏事的同学咯。
代码实现
讲了那么多,又到了
Talk is cheap, Show you my code 的时候了
节点类(Node.cs)
public class Node<T>
{
/// <summary>
///子节点
/// </summary>
public Node<T> Next { get; set; }
/// <summary>
/// 数据
/// </summary>
public T Data { get; set; }
///<summary>
///构造函数
///<summary>
public Node(T data)
{
this.Data = data;
}
}
在这里,定义了个泛型的节点类,里面只有两个属性,一个是子节点,一个是当前节点的数据,还有定义了一个初始化节点数据的构造函数,因为这个类简单,就没啥好说的了
链表类(LinkedList.cs)
public class LinkedList<T>
{
///<sumarry>
///链表是否为空
///<sumarry>
public bool IsEmpty { get { return _count == 0; } }
/// <summary>
/// 链表长度
/// </summary>
public int Count { get { return _count; } }
/// <summary>
/// 头节点
/// </summary>
public Node<T> Head { get { return _head; } }
/// <summary>
/// 尾节点
/// </summary>
public Node<T> Tail { get { return _tail; } }
/// <summary>
/// 链表长度
/// </summary>
private int _count = 0;
/// <summary>
/// 链表头节点
/// </summary>
private Node<T> _head;
/// <summary>
/// 链表尾节点
/// </summary>
private Node<T> _tail;
}
这里就是基本的一个单链表需要的属性,这里也没有什么特别需要解释的了,看代码注释就可以咯。
下面的部分,就开始介绍一个链表的CRUD操作啦。
增加节点
/// <summary>
/// 在尾部添加节点
/// </summary>
/// <param name="node"></param>
public void Append(Node<T> node)
{
if (IsEmpty)
{
_head = node;
_tail = node;
_head.Next = _tail;
}
else
{
_tail.Next = node;
_tail = node;
}
_count += 1;
}
/// <summary>
/// 插入节点
/// </summary>
/// <param name="index">插入位置</param>
/// <param name="node">插入的节点</param>
public void Insert(int index, Node<T> node)
{
if (index < 0)
{
throw new ArgumentException("Index can not less than 0.");
}
else if (index >= Count && Count != 0)
{
throw new ArgumentException("Index out of range.");
}
else if (node == null)
{
throw new ArgumentException("Node can not be null.");
}
if (_count == 0)
{
_head = node;
_tail = node;
_head.Next = _tail;
}
else
{
Node<T> current_node = this[index];
if (current_node == _head)
{
_tail = node;
_head.Next = node;
}
else if (current_node == _tail)
{
current_node.Next = node;
_tail = node;
}
else
{
Node<T> next_node = current_node.Next;
current_node.Next = node;
node.Next = next_node;
}
}
_count += 1;
}
增加节点这里,我分为了2种方式,一种是在链表某个位置插入,另外一种,就是直接在链表的尾部添加。
对于第一种插入节点,需要注意给定的插入位置是否超出链表的范围,比如超出链表长度,或者为负数。
而第二种,则需要注意链表是否只有一个节点,因为只有一个节点的时候,头节点也是尾节点,所以不能直接将新节点赋予给尾节点,作为尾节点的子节点。
删除节点
/// <summary>
/// 删除节点
/// </summary>
/// <param name="node">删除的节点</param>
public void Delete(Node<T> node)
{
//删除头节点
if (node == _head)
{
_head = node.Next;
}
Node<T> previous_node = FindPreviousNode(node);
if (previous_node == null)
{
throw new ArgumentException("Can not find node.");
}
previous_node.Next = node.Next;
//删除尾节点
if (node == _tail)
{
_tail = previous_node;
}
_count -= 1;
}
/// <summary>
/// 删除节点
/// </summary>
/// <param name="index">删除第index个节点</param>
public void Delete(int index)
{
if (index >= Count)
{
throw new ArgumentException("Index out of range.");
}
//头节点
if (index == 0)
{
_head = _head.Next;
}
else
{
Node<T> node = this[index - 1];
//尾节点
if (index == Count - 1)
{
node.Next = null;
}
//其他节点
else
{
node.Next = node.Next.Next;
}
}
_count -= 1;
}
因为删除节点,需要先获取它的前一个节点,所以这里写了一个私有函数,专门用于获取前一节点的。
///<summary>
///获取前一节点
///<summary>
private Node<T> FindPreviousNode(Node<T> node)
{
Node<T> current_node = _head;
while (current_node != null)
{
if (current_node.Next == node)
{
return current_node;
}
current_node = current_node.Next;
}
return null;
}
删除节点,我也提供了2种方式,一种是按照index删除(第index个元素),第二种是删除给定节点,要注意的地方,也是头结点和尾节点。
修改节点值
修改节点的值,这里就不贴代码了,因为可以通过下面查询节点,然后对节点的数据值进行修改。
查询节点
/// <summary>
/// 索引器
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Node<T> this[int index]
{
get
{
if (index >= Count)
{
throw new ArgumentException("Index out of range.");
}
Node<T> node = _head;
while (index > 0)
{
node = node.Next;
index--;
}
return node;
}
}
这里我实现了一个索引器,要注意的点,基本就是不能让给定的索引位置(index)超出链表的长度。
可以看到,这里要获取第N个元素,就像我之前说的那样,必须从第0个元素开始遍历,一直到第N个,所以时间复杂度为O(n),
如果要修改元素,直接通过索引器获取,然后修改就可以啦!
自定义ToString方法
///<sumarry>
///重写ToString方法
/// <param name="count">输出节点个数</param>
///<sumarry>
public string ToString(int count)
{
if (count > Count)
{
throw new ArgumentException("Index out of range.");
}
else if (count == 0)
{
throw new ArgumentException("Output count should more than 0.");
}
StringBuilder sb = new StringBuilder();
int index = 0;
Node<T> current_node = _head;
while (index < count)
{
if (index == count - 1)
{
sb.Append(current_node.Data.ToString());
}
else
{
sb.Append(current_node.Data.ToString() + "->");
}
current_node = current_node.Next;
index++;
}
if (count < Count)
{
sb.Append("->...");
}
return sb.ToString();
}
这个ToString方法,我定义了一个参数,用于设定需要输出的节点个数的,当然这个参数也不能超过链表长度啦。
它的输出样式为1->2->3…
总结
单链表应该是数据结构里面最简单的了,也是我写的最多的一个数据结构,讲真,我从C到C++到Python,到现在的C#版本的单链表我都写过了,用C和C++去写链表,可以更加清晰地理解到指针,也是非常有好处的,如果作为读者的你也有兴趣,可以尝试写一下C或者C++版本的,一定会受益匪浅的。
上述的代码和内容,如果有建议的话,我非常愿意接纳的,希望看到你们的评论。
不知不觉,这篇博客,从码代码到码字,已经花了3小时,希望2020年我的第一篇博客,你们看到之后有所收获!