数据结构之单链表(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年我的第一篇博客,你们看到之后有所收获!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值