《数据结构与算法之美》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、利用哨兵来简化编码实现,你是否还能够想到其他场景,利用哨兵可以大大地简化编码难度?

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

 

posted @ 2020-06-16 18:12  大杂草  阅读(232)  评论(0编辑  收藏  举报