zoukankan      html  css  js  c++  java
  • 《数据结构与算法之美》04——链表

    一、概念

    链表:通过指针将一组零散的内存块串联起来使用。

    数组vs链表

    二、常见链表

    三种常见的链表:单链表、双向链表、循环链表。

    单链表:

     

    时间复杂度:

    插入和删除:O(1)

    随机访问:O(n)

      

    循环链表:

     

    一种特殊的单链表,跟单链表唯一的区别在尾结点:单链表尾结点是Null、循环链表尾结点指向头结点。

    和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。

    时间复杂度:

    插入和删除:O(1)

    随机访问:O(n)

      

    双向链表:

     

    通过额外空间来存储后继结点和前驱结点的地址。正是这种特点,使得双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。这里有个更加重要的知识点:用空间换时间

    双向循环链表:

     

      

    三、链表VS数组性能大比拼

     

    优点

    缺点

    数组

    简单易用;

    可借助CPU的缓存机制;

    访问效率更高;

    大小固定;

    链表

    大小没限制,支持动态扩容

    CPU缓存不好友,无法预读取;

    四、如何实现LRU缓存淘汰算法

    维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,从链表头开始顺序遍历链表。

    1.如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。

    2.如果此数据没有在缓存链表中,又可以分为两种情况:

    如果此时缓存未满,则将此结点直接插入到链表的头部;

    如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

    五、写链表代码六个技巧

    技巧一:理解指针或引用的含义

    指针:将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

    技巧二:警惕指针丢失和内存泄漏

    插入结点时,一定要注意操作的顺序;删除链表结点时,也一定要记得手动释放内存空间(对于有自动管理内存的编程语言来说,就不需要考虑这么多了)

    技巧三:利用哨兵简化实现难度

    针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。

     

    技巧四:重点留意边界条件处理

    检查点:

    1、如果链表为空时,代码是否能正常工作?

    2、如果链表只包含一个结点时,代码是否能正常工作?

    3、如果链表只包含两个结点时,代码是否能正常工作?

    4、代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

    技巧五:举例画图,辅助思考

    技巧六:多写多练,没有捷径

    5个常见的链表操作:

    1、单链表反转

    解题思路:遍历结点,把当前结点插入到头结点,并对受影响的结点(前结点、原头部结点等)的Next重新设置。

    public class ReversedLinkedList
        {
            public static void Run()
            { 
                // 空链表
                MyLinkedNode empty = Init(new int[] { });
                Console.WriteLine("初始链表:");
                Show(empty);
                Console.WriteLine("反转链表:");
                Show(Reversed(empty));
     
                // 单结点
                MyLinkedNode single = Init(new int[] { 1 });
                Console.WriteLine("初始链表:");
                Show(single);
                Console.WriteLine("反转链表:");
                Show(Reversed(single));
                
     
                // 正常链表
                MyLinkedNode normal = Init(new int[] { 1, 2, 3, 4, 5, 6, 7 });
                Console.WriteLine("初始链表:");
                Show(normal);
                Console.WriteLine("反转链表:");
                Show(Reversed(normal));
            }
     
            /// <summary>
            /// 初始化链表
            /// </summary>
            /// <param name="datas"></param>
            /// <returns></returns>
            public static MyLinkedNode Init(int[] datas)
            {
                MyLinkedNode head = new MyLinkedNode(-1);
                MyLinkedNode curr = head;
     
                for (int i = 0; i < datas.Length; i++)
                {
                    var node = new MyLinkedNode(datas[i]);
     
                    curr.Next = node;
                    curr = node;
                }
     
                return head;
            }
     
            public static MyLinkedNode Reversed(MyLinkedNode head)
            {
                if (head == null || head.Next == null)
                {
                    return head;
                }
     
                // 首结点不用操作,从第二个结点开始
                MyLinkedNode curr = head.Next.Next;
                MyLinkedNode prev = head.Next;
     
                while (curr != null)
                {
                    MyLinkedNode node1 = head.Next;
                    MyLinkedNode node2 = curr.Next;
     
                    // 将当前结点插入到头结点
                    head.Next = curr;
                    // 将当前结点的Next设置为原来的头结点,插入头结点完成
                    curr.Next = node1;
                    // 前结点的Next设置为当前结点的原Next,把当前结点的后续结点链接起来
                    prev.Next = node2;
     
                    // 当前结点移动到下一结点
                    curr = prev.Next;
                }
     
                return head;
            }
     
            public static void Show(MyLinkedNode head)
            {
                MyLinkedNode curr = head.Next;
     
                string msg = "序列:";
     
                while (curr != null)
                {
                    msg += string.Format("{0} ", curr.Data);
                    curr = curr.Next;
                }
     
                Console.WriteLine(msg);
            }
     
            public class MyLinkedNode
            {
                public MyLinkedNode(int data)
                {
                    Data = data;
                }
     
                public int Data { get; set; }
     
                public MyLinkedNode Next { get; set; }
            }
        }
    

    时间复杂度:O(n)

    空间复杂度:O(n)

    2、链表中环的检测

    解题思路:设置两个步长,分别+1+2,当出现结点相同时,表示有环。

    类比,有两个跑步运动员AB,分别以1m/s2m/s的速度在同一个跑道上跑步,当B能追上A时,表示跑道是环,如果B不能追上A,表示非环。

    public class CircleLinkedList
        {
            public static void Run()
            {
                // 空链表
                MyLinkedNode empty = Init(new int[] { }, false);
                Console.WriteLine("是否环:{0}", IsCircle(empty));
     
                // 单结点
                MyLinkedNode single = Init(new int[] { 1 }, false);
                Console.WriteLine("是否环:{0}", IsCircle(single));
     
                // 非环
                MyLinkedNode notcircle = Init(new int[] { 1, 2, 3, 4, 5 }, false);
                Console.WriteLine("是否环:{0}", IsCircle(notcircle));
     
                // 环
                MyLinkedNode circle = Init(new int[] { 1, 2, 3, 4, 5 }, true);
                Console.WriteLine("是否环:{0}", IsCircle(circle));
            }
     
            /// <summary>
            /// 初始化链表
            /// </summary>
            /// <param name="datas"></param>
            /// <returns></returns>
            public static MyLinkedNode Init(int[] datas, bool circle)
            {
                MyLinkedNode head = new MyLinkedNode(-1);
                MyLinkedNode curr = head;
     
                //int[] datas = new int[] { 1, 2, 3, 4, 5 };
                for (int i = 0; i < datas.Length; i++)
                {
                    var node = new MyLinkedNode(datas[i]);
     
                    curr.Next = node;
                    curr = node;
                }
     
                if (circle)
                {
                    curr.Next = head.Next;
                }
     
                return head;
            }
     
            public static bool IsCircle(MyLinkedNode head)
            {
                // 当链表为空或者只有一个结点时,返回false
                if (head == null || head.Next == null || head.Next.Next == null)
                {
                    return false;
                }
     
                MyLinkedNode stepA = head.Next;
                MyLinkedNode stepB = head.Next.Next;
     
                while (stepA != null && stepB != null)
                {
                    if (stepA == stepB)
                    {
                        return true;
                    }
     
                    // 设置步长+1
                    stepA = stepA.Next;
     
                    // 当下一下结点为Null,结束循环,跳出
                    if (stepB.Next == null)
                    {
                        break;
                    }
                    else
                    {
                        // 设置步长+2
                        stepB = stepB.Next.Next;
                    }
                }
     
                return false;
            }
     
            public static void Show(MyLinkedNode head)
            {
                MyLinkedNode curr = head.Next;
     
                string msg = "序列:";
     
                while (curr != null)
                {
                    msg += string.Format("{0} ", curr.Data);
                    curr = curr.Next;
                }
     
                Console.WriteLine(msg);
            }
     
            public class MyLinkedNode
            {
                public MyLinkedNode(int data)
                {
                    Data = data;
                }
     
                public int Data { get; set; }
     
                public MyLinkedNode Next { get; set; }
            }
        }
    

    时间复杂度:O(n)

    空间复杂度:O(n)

    3、两个有序的链表合并

    解题思路:遍历两个链表,把结点往新链表尾部插入。当某个链表遍历完,把另一个链表直接插入到新链表的尾部。

    /// <summary>
    /// 两个有序的链表合并
    /// </summary>
    public class MergeLinkedList
    {
     
        public static void Run()
        {
            // 两个为空链表
            MyLinkedNode emptyA = Init(new int[] { });
            MyLinkedNode emptyB = Init(new int[] { });
            Show(Merge(emptyA, emptyB));
     
            // 一个为空链表
            MyLinkedNode emptyC = Init(new int[] { });
            MyLinkedNode linkedD = Init(new int[] { 1, 2, 4, 5, 6 });
            Show(Merge(emptyC, linkedD));
     
            // 两个非空链表
            MyLinkedNode linkedE = Init(new int[] { 1, 3, 4, 5, 9, 10 });
            MyLinkedNode linkedF = Init(new int[] { 2, 3, 5, 6, 7, 8 });
            Show(Merge(linkedE, linkedF));
        }
     
        /// <summary>
        /// 初始化链表
        /// </summary>
        /// <param name="datas"></param>
        /// <returns></returns>
        private static MyLinkedNode Init(int[] datas)
        {
            MyLinkedNode head = new MyLinkedNode(-1);
            MyLinkedNode curr = head;
     
            foreach (var data in datas)
            {
                var node = new MyLinkedNode(data);
     
                curr.Next = node;
                curr = node;
            }
     
            return head;
        }
     
        private static MyLinkedNode Merge(MyLinkedNode headA, MyLinkedNode headB)
        {
            if (headA == null || headA.Next == null)
            {
                return headB;
            }
     
            if (headB == null || headB.Next == null)
            {
                return headA;
            }
     
            MyLinkedNode head = new MyLinkedNode(-1);
            MyLinkedNode curr = head;
     
            MyLinkedNode currA = headA.Next;
            MyLinkedNode currB = headB.Next;
     
            while (currA != null && currB != null)
            {
                if (currA.Data <= currB.Data)
                {
                    curr.Next = currA;
                    currA = currA.Next;
                }
                else
                {
                    curr.Next = currB;
                    currB = currB.Next;
                }
     
                curr = curr.Next;
                curr.Next = null;
            }
     
            curr.Next = currA ?? currB;
     
            return head;
        }
     
        private static void Show(MyLinkedNode head)
        {
            MyLinkedNode curr = head.Next;
     
            string msg = "序列:";
     
            while (curr != null)
            {
                msg += string.Format("{0} ", curr.Data);
                curr = curr.Next;
            }
     
            Console.WriteLine(msg);
        }
     
     
        public class MyLinkedNode
        {
            public MyLinkedNode(int data)
            {
                Data = data;
            }
     
            public int Data { get; set; }
     
            public MyLinkedNode Next { get; set; }
        }
    }
    

    时间复杂度:O(n)

    空间复杂度:O(n)

    4、删除链表倒数第n个结点

    解题思路:反转链表,遍历到第n-1个结点,链接到第n+1个结点(即删除第n个结点),再反转链表。

    public class RemoveLinkedList
    {
        public static void Run()
        {
            // 删除第4个结点,即下标是3的结点
            int index = 3;
     
            // 空链表
            MyLinkedNode empty = Init(new int[] { });
            Console.Write("初始链表");
            Show(empty);
            Console.WriteLine("删除倒数第{0}个结点:{1}", index + 1, RemoveAt(empty, index));
            Show(empty);
     
            // 单结点
            MyLinkedNode single = Init(new int[] { 1 });
            Console.Write("初始链表");
            Show(single);
            Console.WriteLine("删除倒数第{0}个结点:{1}", index + 1, RemoveAt(single, index)); 
            Show(single);
     
     
            // 正常链表
            MyLinkedNode normal = Init(new int[] { 1, 2, 3, 4, 5, 6, 7 });
            Console.Write("初始链表");
            Show(normal);
            Console.WriteLine("删除倒数第{0}个结点:{1}", index + 1, RemoveAt(normal, index)); 
            Show(normal);
        }
     
        /// <summary>
        /// 初始化链表
        /// </summary>
        /// <param name="datas"></param>
        /// <returns></returns>
        private static MyLinkedNode Init(int[] datas)
        {
            MyLinkedNode head = new MyLinkedNode(-1);
            MyLinkedNode curr = head;
     
            for (int i = 0; i < datas.Length; i++)
            {
                var node = new MyLinkedNode(datas[i]);
     
                curr.Next = node;
                curr = node;
            }
     
            return head;
        }
     
        private static bool RemoveAt(MyLinkedNode head, int index)
        {
            var reversed = Reversed(head);
     
            int count = 0;
     
            MyLinkedNode curr = reversed.Next;
     
            while (curr != null)
            {
                count++;
     
                if (count == index)
                {
                    curr.Next = curr.Next.Next;
                    break;
                }
     
                curr = curr.Next;
            }
     
            head = Reversed(reversed);
     
            return count >= index;
        }
     
        private static MyLinkedNode Reversed(MyLinkedNode head)
        {
            if (head == null || head.Next == null)
            {
                return head;
            }
     
            // 首结点不用操作,从第二个结点开始
            MyLinkedNode curr = head.Next.Next;
            MyLinkedNode prev = head.Next;
     
            while (curr != null)
            {
                MyLinkedNode node1 = head.Next;
                MyLinkedNode node2 = curr.Next;
     
                // 将当前结点插入到头结点
                head.Next = curr;
                // 将当前结点的Next设置为原来的头结点,插入头结点完成
                curr.Next = node1;
                // 前结点的Next设置为当前结点的原Next,把当前结点的后续结点链接起来
                prev.Next = node2;
     
                // 当前结点移动到下一结点
                curr = prev.Next;
            }
     
            return head;
        }
     
        private static void Show(MyLinkedNode head)
        {
            MyLinkedNode curr = head.Next;
     
            string msg = "序列:";
     
            while (curr != null)
            {
                msg += string.Format("{0} ", curr.Data);
                curr = curr.Next;
            }
     
            Console.WriteLine(msg);
        }
     
        public class MyLinkedNode
        {
            public MyLinkedNode(int data)
            {
                Data = data;
            }
     
            public int Data { get; set; }
     
            public MyLinkedNode Next { get; set; }
        }
    }
    

    时间复杂度:O(n)

    空间复杂度:O(n)

    5、求链表的中间结点

    解题思路:遍历一次,计算链表元素个数,然后算出中间结点的下标,转为获取第N个结点。

    public class MiddleLinkedList
    {
        public static void Run()
        {
            MyLinkedNode middleNode = null;
     
            // 空链表
            MyLinkedNode empty = Init(new int[] { });
            Console.Write("初始链表");
            Show(empty);
            middleNode = GetMiddleNode(empty);
            Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
     
            // 单结点
            MyLinkedNode single = Init(new int[] { 1 });
            Console.Write("初始链表");
            Show(single);
            middleNode = GetMiddleNode(single);
            Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
     
     
            // 单数链表
            MyLinkedNode odd = Init(new int[] { 1, 2, 3, 4, 5, 6, 7 });
            Console.Write("初始链表");
            Show(odd);
            middleNode = GetMiddleNode(odd);
            Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
     
            // 双数链表
            MyLinkedNode even = Init(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 });
            Console.Write("初始链表");
            Show(even);
            middleNode = GetMiddleNode(even);
            Console.WriteLine("中间结点:{0}", middleNode == null ? "null" : middleNode.Data.ToString());
        }
     
        /// <summary>
        /// 初始化链表
        /// </summary>
        /// <param name="datas"></param>
        /// <returns></returns>
        private static MyLinkedNode Init(int[] datas)
        {
            MyLinkedNode head = new MyLinkedNode(-1);
            MyLinkedNode curr = head;
     
            for (int i = 0; i < datas.Length; i++)
            {
                var node = new MyLinkedNode(datas[i]);
     
                curr.Next = node;
                curr = node;
            }
     
            return head;
        }
     
        private static MyLinkedNode GetMiddleNode(MyLinkedNode head)
        {
            if (head == null || head.Next == null)
            {
                return null;
            }
     
            int count = 0;
     
            MyLinkedNode curr = head.Next;
     
            while (curr != null)
            {
                count++;
     
                curr = curr.Next;
            }
     
            curr = head;
     
            int middleIndex = count == 1 ? 0 : (count - 1) / 2;
     
            while (middleIndex >= 0)
            {
                middleIndex--;
                curr = curr.Next;
            }
     
            //for (int i = 0; i <= middleIndex; i++)
            //{
            //    curr = curr.Next;
            //}
     
            return curr;
        }
     
        private static void Show(MyLinkedNode head)
        {
            MyLinkedNode curr = head.Next;
     
            string msg = "序列:";
     
            while (curr != null)
            {
                msg += string.Format("{0} ", curr.Data);
                curr = curr.Next;
            }
     
            Console.WriteLine(msg);
        }
     
        public class MyLinkedNode
        {
            public MyLinkedNode(int data)
            {
                Data = data;
            }
     
            public int Data { get; set; }
     
            public MyLinkedNode Next { get; set; }
        }
    }
    

    时间复杂度:O(n)

    空间复杂度:O(n)

    六、课后思考:

    1、如果字符串是通过单链表来存储的,那该如何来判断是一个回文串呢?时间、空间复杂度是多少?

    namespace PalindromeApp
    {
        class Program
        {
            static void Main(string[] args)
            {
                string textA = "abcdcba";
                Console.WriteLine("{0}是否回文:{1}", textA, IsPalindrome(Init(textA)));
     
                string textB = "abcddcba";
                Console.WriteLine("{0}是否回文:{1}", textB, IsPalindrome(Init(textB)));
     
                string textC = "abedcba";
                Console.WriteLine("{0}是否回文:{1}", textC, IsPalindrome(Init(textC)));
     
                Console.Read();
            }
     
            static MyLinkedNode Init(string text)
            {
                MyLinkedNode head = new MyLinkedNode('&');
     
                MyLinkedNode curr = head;
     
                foreach (char c in text)
                {
                    curr.Next = new MyLinkedNode(c);
                    curr = curr.Next;
                }
     
                return head;
            }
     
            static MyLinkedNode Reverse(MyLinkedNode head)
            {
                MyLinkedNode reversed = null;
     
                MyLinkedNode curr = head.Next;
     
                while (curr != null)
                {
                    MyLinkedNode node = new MyLinkedNode(curr.Data);
     
                    node.Next = reversed;
                    reversed = node;
     
                    curr = curr.Next;
                }
     
                MyLinkedNode reversedHead = new MyLinkedNode('&');
                reversedHead.Next = reversed;
     
                return reversedHead;
            }
     
            static bool IsPalindrome(MyLinkedNode head)
            {
                MyLinkedNode reversed = Reverse(head);
     
                Show(head);
                Show(reversed);
     
                MyLinkedNode curr = head.Next;
                MyLinkedNode reversedCurr = reversed.Next;
     
                while (curr != null && reversedCurr != null)
                {
                    if (curr.Data != reversedCurr.Data)
                    {
                        return false;
                    }
     
                    curr = curr.Next;
                    reversedCurr = reversedCurr.Next;
                }
     
                if (curr != null || reversedCurr != null)
                {
                    return false;
                }
     
                return true;
            }
     
     
     
            static void Show(MyLinkedNode head)
            {
                MyLinkedNode curr = head.Next;
     
                string msg = "序列:";
     
                while (curr != null)
                {
                    if (curr.Data == -1)
                    {
                        break;
                    }
     
                    msg += curr.Data;
     
                    curr = curr.Next;
                }
     
                Console.WriteLine(msg);
            }
        }
     
     
        public class MyLinkedNode
        {
            public MyLinkedNode(char data)
            {
                Data = data;
            }
     
            public char Data { get; set; }
     
            public MyLinkedNode Next { get; set; }
        }
    }
    

    时间复杂度:O(n)

    空间复杂度:O(n)

    2、利用哨兵来简化编码实现,你是否还能够想到其他场景,利用哨兵可以大大地简化编码难度?

    哨兵有点像中间件的味道,在两个系统或模块之间的调用时,有时候通过中间件能够简化两边的技术实现差异。

  • 相关阅读:
    CSS3-loading动画(三)
    CSS3-loading动画(二)
    CSS3-loading动画(一)
    CSS reset ---- 个人理解与惯用方式
    HTTP常见状态码 200 301 302 404 500
    c#导出文件,文件名中文乱码解决方法。
    对FineU框架Grid多表头合计行导出Excel的回顾
    JS数据类型
    细数使用View UI(iView)开发中遇到的坑
    源生JS实现点击复制功能
  • 原文地址:https://www.cnblogs.com/liang24/p/13143612.html
Copyright © 2011-2022 走看看