[데이터 구조] 연결리스트의 이중연결리스트

목차

기본 개념 및 작동:

구현 방법 및 원리

적용 시나리오 및 사용상의주의 사항

알고리즘 및 복잡성 분석:

다른 데이터 구조와의 비교:

ps. 틀린 부분이나 누락된 부분이 있으면 정정해주세요


기본 개념 및 작동:

이중 연결 리스트(Double Linked List)는 일반적인 데이터 구조로, 단일 연결 리스트와 비교하여 선행 노드에 대한 포인터를 추가하므로 각 노드는 후행 노드에 대한 포인터 외에 선행 노드에 대한 포인터도 갖습니다. . 이런 방식으로 연결된 목록을 양방향으로 탐색할 수 있으므로 연결 목록을 보다 유연하게 운영할 수 있습니다.

이중 연결 리스트를 사용하면 삽입, 삭제, 검색 등의 작업을 보다 편리하게 구현할 수 있습니다. 단일 연결 리스트에 비해 이중 연결 리스트의 단점은 선행 노드의 포인터를 저장하기 위해 추가 공간이 필요하고 포인터의 정확성을 유지하기 위해 더 많은 코드가 필요하다는 점이다.

이중 연결 리스트의 노드는 C# 언어로 다음과 같이 설명됩니다.

namespace DataStructLibrary
{
    
    /// <summary>
    /// 双链表节点
    /// </summary>
   public class DbNode<T>
    {
        private T data;//数据域
        private DbNode<T> prev;//前驱引用域
        private DbNode<T> next;//后继引用域

        /// <summary>
        /// 构造器
        /// </summary>
        /// <param name="val">数据域</param>
        /// <param name="p">后继引用域</param>
        public DbNode(T val,DbNode<T> p)
        {
            data = val;
            next = p;
        }

        /// <summary>
        /// 构造器
        /// </summary>
        /// <param name="p">后继引用域</param>
        public DbNode(DbNode<T> p)
        {
            next = p;
        }

        /// <summary>
        /// 构造器
        /// </summary>
        /// <param name="val">数据域</param>
        public DbNode(T val)
        {
            data = val;
            next = null;
        }

        /// <summary>
        /// 构造器
        /// </summary>
        public DbNode()
        {
            data = default(T);
            next = null;
        }

        /// <summary>
        /// 数据域属性
        /// </summary>
        public T Data
        {
            get { return data; }
            set { data = value; }
        }

        /// <summary>
        /// 前驱引用域属性
        /// </summary>
        public DbNode<T> Prev
        {
            get { return prev; }
            set { prev = value; }
        }

        /// <summary>
        /// 后继引用域属性
        /// </summary>
        public DbNode<T> Next
        {
            get { return next; }
            set { next = value; }
        }
    }
}

      이중 연결 목록에서 일부 작업(길이 찾기, 요소 가져오기, 위치 지정 등)의 알고리즘은 후속 포인터만 포함하며 연결 ​​목록의 알고리즘은 단일 연결 목록의 알고리즘과 동일합니다. 그러나 전방 삽입 및 삭제 작업의 경우 양방향 연결 목록은 후행 가장자리의 두 포인터를 동시에 수정해야 하며 이는 단일 연결 목록보다 더 복잡합니다.

다음은 이중 연결 목록에서 일반적으로 사용되는 작업입니다.

  1. 이중 연결 리스트 초기화: 이중 연결 리스트를 초기화하면 빈 이중 연결 리스트가 생성되며, 생성 과정은 다음 표와 같습니다.

단계 작동하다
1 노드 유형의 시작 변수를 선언하십시오.
2

이중 연결 리스트의 생성자에서 시작 변수의 값을 null로 할당합니다.

     2. 삽입 작업: 연결 리스트의 지정된 위치(예: 머리, 꼬리 또는 가운데)에 새 노드를 삽입합니다.

         2.1 단일 연결 리스트의 시작 부분에 새 노드 삽입

        2.2 연결리스트의 두 노드 사이에 노드 삽입

        2.3 연결리스트 끝에 새 노드 삽입

구체적인 구현 과정은 다음과 같습니다.

단계 작동하다
1

새 노드에 메모리를 할당하고 새 노드의 데이터 필드에 값을 할당합니다.

2

목록이 비어 있으면 목록에 노드를 삽입하기 위해 다음 단계가 수행됩니다.

a) 새 노드의 다음 필드가 null을 가리키도록 만듭니다.

b) 새 노드의 prev 필드를 null로 설정합니다.
c) 새 노드의 시작점을 만듭니다.

목록의 시작 부분에 노드를 삽입하려면 다음 단계를 수행하십시오.

a) 새 노드의 다음 필드가 목록의 첫 번째 노드를 가리키도록 만듭니다.

b) 시작의 이전 필드가 새 노드를 지정하도록 합니다.

c) 새 노드의 prev 필드가 null을 가리키도록 만듭니다.

d) 새 노드를 지정하기 시작합니다.

4

두 개의 기존 노드 사이에 새 노드를 삽입하려면 다음 단계를 수행하십시오.

a) 새 노드의 다음 노드가 현재 노드를 가리키도록 만듭니다.

b) 새 노드의 이전 노드가 이전 노드를 가리키도록 만듭니다.

c) 현재 노드의 이전 노드가 새 노드를 가리키도록 만듭니다.

d) 이전 노드의 다음 노드가 새 노드를 가리키도록 만듭니다.

5

목록 끝에 새 노드를 삽입하고, 현재 포인터를 마지막 노드로 이동할 때 다음 단계를 수행합니다.

a) 현재 노드의 다음 노드가 새 노드를 지정하도록 합니다.

b) 새 노드의 이전 노드가 현재 노드를 가리키도록 만듭니다.

c) 다음 새 노드를 null로 만듭니다.

      3. 삭제 작업

      이중 연결 리스트에서 노드를 삭제하는 구체적인 알고리즘은 다음과 같습니다.

단계 작동하다
1

삭제할 노드를 찾아 삭제할 노드를 현재 노드로 표시합니다.

2

삭제된 노드가 첫 번째 노드인 경우 현재 노드의 다음 노드로 직접 시작점을 만듭니다.

삭제할 노드가 두 노드 사이의 노드인 경우 다음 단계를 수행하십시오.

a) 이전 노드의 다음 필드가 현재 노드의 다음 노드를 가리키도록 만듭니다.

b) 현재 노드 뒤의 노드의 prev 필드가 이전 노드를 가리키도록 만듭니다.

c) 현재 노드로 표시된 노드 메모리 해제

4 삭제된 노드가 마지막 노드인 경우 a) 및 c) 단계를 수행합니다. 여기서 삭제된 노드는 두 노드 사이의 노드입니다.

    4. 이중 연결 목록의 모든 노드를 탐색합니다.

이중 연결 목록을 사용하면 정방향과 역방향으로 목록을 탐색할 수 있습니다. 목록을 정방향으로 순회하는 알고리즘은 다음과 같습니다.

    이중 연결 리스트를 역순으로 탐색하는 알고리즘은 다음과 같습니다.

이중 연결 목록을 사용하면 정방향과 역방향으로 목록을 탐색할 수 있습니다. 목록을 정방향으로 순회하는 알고리즘은 다음과 같습니다.

테이블의 길이를 계산하고 테이블이 비어 있는지 판단하는 등 선형 테이블과 관련된 다른 작업은 순차 테이블에서 구현하는 것이 상대적으로 간단합니다. 이중 연결 목록에 대한 다음 C# 코드를 참조하세요.

/// <summary>
    /// 双链表数据结构实现接口具体步骤
    /// </summary>
    public class DbLinkList<T>:ILinarList<T>
    {
        private DbNode<T> start;//双向链表的头引用
        private int length;//双向链表的长度

        /// <summary>
        /// 初始化双向链表
        /// </summary>
        public DbLinkList()
        {
            start = null;
        }


        /// <summary>
        /// 在双链表的末尾追加数据元素 data
        /// </summary>
        /// <param name="data">数据元素</param>
        public void InsertNode(T data)
        {
            DbNode<T> newnode = new DbNode<T>(data);
            if (IsEmpty())
            {
                start = newnode;
                length++;
                return;
            }
            DbNode<T> current = start;
            while (current.Next!= null)
            {
                current = current.Next;
            }
            current.Next = newnode;
            newnode.Prev = current;
            newnode.Next = null;
            length++;
        }


        /// <summary>
        /// 在双链表的第i个数据元素的位置前插入一个数据元素data
        /// </summary>
        /// <param name="data">数据元素</param>
        /// <param name="i">第i个数据元素的位置</param>
        public void InsertNode(T data, int i)
        {
            DbNode<T> current;
            DbNode<T> previous;
            if (i < 1)
            {
                Console.WriteLine("Position is error");
                return;
            }
            DbNode<T> newNode = new DbNode<T>(data);
            //在空链表或第一个元素前插入第一个元素
            if (i == 1)
            {
                newNode.Next = start;
                start = newNode;
                length++;
                return;
            }
            //在双链表的两个元素间插入一个元素
            current = start;
            previous = null;
            int j = 1;
            while(current != null && j < i)
            {
                previous = current;
                current = current.Next;
                j++;
            }
            if (j == i)
            {
                newNode.Next = current;
                newNode.Prev = previous;
                if(current!= null)
                {
                    current.Prev = newNode;
                    previous.Next = newNode;

                }
                length++;
            }
        }

        /// <summary>
        /// 删除双链表的第i个数据元素
        /// </summary>
        /// <param name="i"></param>
        public void DeleteNode(int i)
        {
            if(IsEmpty() || i < 1)
            {
                Console.WriteLine("Link is empty or Position is error!");
            }
            DbNode<T> current = start;
            if(i==1)
            {
                start = current.Next;
                length--;
                return;
            }
            DbNode<T> previous = null;
            int j = 1;
            while(current.Next != null && j<i)
            {
                previous = current;
                current = current.Next;
                j++;
            }
            if (j == i)
            {
                previous.Next = current.Next;
                if(current.Next!= null)
                {
                    current.Next.Prev = previous;
                   
                }
                previous = null;
                current = null;
                length--;
            }
            else
            {
                Console.WriteLine("The ith node is not exist!");
            }
        }


        /// <summary>
        /// 获得双链表的第i个数据元素
        /// </summary>
        /// <param name="i"></param>
        /// <returns></returns>
        public T SearchNode(int i)
        {
            if (IsEmpty())
            {
                Console.WriteLine("List is empty!");
                return default(T);
            }
            DbNode<T> current = start;
            int j = 1;
            while(current.Next != null && j<i)
            {
                current = current.Next;
                j++;
            }
            if (j == i)
            {
                return current.Data;
            }
            else
            {
                Console.WriteLine("The ith node is not exist!");
                return default(T);
            }
        }


        /// <summary>
        /// 在双链表中查找值为data的数据元素
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public T SearchNode(T data)
        {
            if (IsEmpty())
            {
                Console.WriteLine("List is Empty");
                return default(T);
            }
            DbNode<T> current = start;
            int i = 1;
            while (current != null && !current.Data.Equals(data))
            {
                current = current.Next;
                i++;
            }
            if(current != null)
            {
                return current.Data;
            }
            return default(T);
        }

        /// <summary>
        /// 获取双链表的长度
        /// </summary>
        /// <returns></returns>
        public int GetLength()
        {
            return length;
        }

        /// <summary>
        /// 清空链表
        /// </summary>
        public void Clear()
        {
            start = null;
        }

        /// <summary>
        /// 判断链表是否为空
        /// </summary>
        /// <returns></returns>
        public bool IsEmpty()
        {
            if (start == null)
            {
                return true;
            }
            return false;
        }


        /// <summary>
        /// 该函数将链表头节点反转后,重新作为链表的头节点。算法使用迭代方式实现,遍历链表并改变指针指向
        /// 例如链表头节点start:由原来的
        /// data:a,
        /// prev:null,
        /// next:[
        ///      data:b,
        ///      prev:a,
        ///      next:[
        ///           data:c,
        ///           prev:b,
        ///           next:null
        ///          ]
        ///     ] 
        ///翻转后的结果为:
        /// data:c,
        /// prev:null,
        /// next:[
        ///      data:b,
        ///      prev:c,
        ///      next:[
        ///           data:a,
        ///           prev:b,
        ///           next:null
        ///          ]
        ///     ] 
        /// </summary>
        public void ReverseList()
        {
            if (length == 1 || IsEmpty())
            {
                return;
            }
            //定义 previous next 两个指针
            DbNode<T> previous = null;
            DbNode<T> next = null;
            DbNode<T> current = this.start;
            //循环操作
            while (current != null)
            {
                //定义next为Head后面的数,定义previous为Head前面的数
                next = current.Next;
                current.Prev = next;
                current.Next = previous;//这一部分可以理解为previous是Head前面的那个数。
                //然后再把previous和Head都提前一位
                previous = current;
                current = next;
            }
            this.start = previous;
            //循环结束后,返回新的表头,即原来表头的最后一个数。
            return;
        }
    }

구현 방법 및 원리

이중 연결 리스트를 구현할 때는 포인터 정확성, 메모리 관리 등의 문제에 주의해야 합니다. 성능과 확장성을 향상시키기 위해 Sentinel 노드, 순환 연결 목록과 같은 기술을 사용하여 이중 연결 목록을 최적화할 수 있습니다.

  1. 이중 연결 리스트는 일반적인 데이터 구조로, 단일 연결 리스트와 비교하여 선행 노드에 대한 포인터를 추가하므로 각 노드에는 후속 노드에 대한 포인터 외에도 선행 노드에 대한 포인터가 있습니다. 다음은 이중 연결 목록의 구현 방법과 원리를 소개하며 주로 다음 측면을 포함합니다.

  2. 노드 정의: 이중 연결 목록의 각 노드에는 세 가지 기본 요소가 포함되어야 합니다. 하나는 데이터 저장을 위한 변수이고 다른 두 개는 선행 노드와 후속 노드에 대한 포인터입니다.

  3. 헤드 노드와 테일 노드: 헤드 노드와 테일 노드는 이중 연결 리스트의 두 개의 특수 노드입니다. 헤드 노드에는 선행 노드가 없고 테일 노드에는 후속 노드가 없습니다.

  4. 添加操作:向双向链表中添加新节点,需要创建一个新节点,并将其插入到链表的合适位置上,同时设置新节点的前驱和后继指针。

  5. 删除操作:从双向链表中删除节点,需要找到待删除节点的前驱节点和后继节点,然后修改它们的前驱和后继指针,使其不再指向待删除节点。

  6. 查找操作:查找双向链表中的某个节点,可以从链表的头节点或尾节点开始遍历整个链表,直到找到目标节点或遍历完整个链表。

  7. 遍历操作:遍历双向链表,可以从链表的头节点或尾节点开始,依次输出每个节点的数据。

  8. 长度统计:统计双向链表中节点的数量,可以通过遍历链表并计数的方式来实现。

了解单链表的实现方法和原理对于理解链表性能和优化有很大帮助。同时,需要注意在具体实现时要根据实际情况进行合理的设计和优化,以提高代码的效率和可维护性。

应用场景和使用注意事项

单链表是一种常见的数据结构,常用于以下应用场景:

  1. 实现栈和队列:单链表可以用来实现栈和队列。在栈中,元素只能从栈顶进出;在队列中,元素只能从队尾进,队首出。利用单链表的头插和尾插操作,可以很方便地实现这两种数据结构。

  2. 内存分配:在计算机内存管理中,单链表常被用作动态内存分配的数据结构。通过链表节点之间的指针连接,可以动态地申请和释放内存块。

  3. 音频和视频播放列表:单链表可以用来实现音频和视频播放列表。每个节点表示一个音频或视频文件,并保存下一个文件的位置。通过遍历链表,可以依次播放整个列表中的音频和视频。

  4. 寻找环形结构:单链表也可以用来处理关于环形结构的问题,例如判断一个链表是否有环、找到环的入口等。

  5. 缓存淘汰策略:在缓存系统中,当缓存空间已满时,需要淘汰一些数据来腾出空间。单链表可以用来维护缓存中的数据项,同时记录它们的使用情况。当需要淘汰数据时,可以选择最近最少使用的数据项进行淘汰,即删除单链表尾部的节点。

在使用单链表时需要注意以下几点:

  1. 空指针问题:在链表操作中容易出现空指针问题,例如访问空链表或者一个不存在的节点等。为了避免这些问题,需要对输入参数进行判空处理。

  2. 内存管理问题:在插入和删除节点时需要分配或释放内存空间,如果管理不当容易出现内存泄漏或者重复释放等问题。可以使用垃圾回收机制或手动管理内存空间来解决这些问题。

  3. 边界条件问题:在进行链表操作时需要处理一些边界条件和异常情况,例如链表为空、插入位置超出范围等情况,以确保链表的正常运行。

  4. 性能问题:在处理大规模数据时,单链表会存在一些性能问题,例如随机访问速度较慢、空间开销较大等。因此在实际应用中需要根据实际情况选择合适的数据结构。

算法和复杂度分析:

常见的双向链表算法包括插入、删除和查找操作,下面对它们进行简要分析:

  1. 插入操作:在双向链表中插入一个节点时,需要找到待插入位置的前驱节点和后继节点,并修改它们的指针。时间复杂度为O(n)。

  2. 删除操作:在双向链表中删除一个节点时,也需要找到待删除节点的前驱节点和后继节点,并修改它们的指针。时间复杂度为O(n)。

  3. 查找操作:在双向链表中查找一个节点时,可以从头节点或尾节点开始遍历整个链表,直到找到目标节点或遍历完整个链表。时间复杂度为O(n)。

  4. 排序算法:双向链表可以使用插入排序、冒泡排序等算法进行排序,排序的时间复杂度取决于具体算法实现。

另外,在实际应用中,可以通过使用哨兵节点、循环链表等技术来优化双向链表的性能和扩展性。

总之,双向链表是一种常见的数据结构,具有良好的灵活性和扩展性。在实际使用中,需要根据具体情况选择合适的算法,并注意内存管理和指针操作。

与其他数据结构的比较:

双向链表是一种常见的数据结构,与其他数据结构相比较有以下优点和缺点:

  1. 与数组比较:双向链表可以动态增加和删除节点,不需要预先分配固定大小的空间,因此具有更好的灵活性。但是,双向链表的访问时间复杂度为O(n),而数组的访问时间复杂度为O(1),因此在随机访问和遍历操作比较频繁的情况下,数组可能更加高效。

  2. 与队列比较:双向链表和队列都可以实现FIFO(先进先出)的数据结构,但是双向链表相比队列更加灵活,可以支持在任意位置插入和删除节点,而队列只能在头部插入和尾部删除元素。

  3. 与栈比较:双向链表和栈都可以实现FILO(先进后出)的数据结构,但是双向链表相比栈更加灵活,可以支持在任意位置插入和删除节点,而栈只能在栈顶插入和删除元素。

  4. 与哈希表比较:双向链表和哈希表都是常用的数据结构,但是它们的应用场景不同。哈希表适用于快速查找和插入键值对的场景,而双向链表适用于需要频繁插入和删除元素的场景。

总之,双向链表具有一些特点,如动态性、灵活性等优点,但是在访问效率等方面可能不如其他数据结构。在实际应用中,需要根据具体情况选择合适的数据结构,并进行合理的权衡和折衷。

PS:如有错漏之处,敬请指正

추천

출처blog.csdn.net/beenles/article/details/131432770