目次
基本的な概念と操作:
ダブル リンク リスト (Doubly 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; }
}
}
}
二重リンク リストでは、一部の操作 (長さの検索、要素のフェッチ、位置決めなど) のアルゴリズムには後続ポインターのみが含まれ、リンク リストのアルゴリズムは単一リンク リストのアルゴリズムと同じです。ただし、前方への挿入および削除操作の場合、双方向リンク リストでは後端の 2 つのポインタを同時に変更する必要があり、これは単一リンク リストよりも複雑です。
二重リンクリストに対して一般的に使用される操作は次のとおりです。
-
二重リンクリストの初期化: 二重リンクリストの初期化とは、空の二重リンクリストを作成することです。作成プロセスを次の表に示します。
ステップ | 操作する |
---|---|
1 | ノード型の開始変数を宣言します。 |
2 | 二重リンクリストのコンストラクターで開始変数の値を null に代入します。 |
2. 挿入操作: リンクリストの指定された位置 (先頭、末尾、中央など) に新しいノードを挿入します。
2.1 単一リンクリストの先頭に新しいノードを挿入する
2.2 リンクリスト内の 2 つのノードの間にノードを挿入する
2.3 リンクリストの最後に新しいノードを挿入する
具体的な実装プロセスは以下の通りです。
ステップ | 操作する |
---|---|
1 | 新しいノードにメモリを割り当て、新しいノードのデータ フィールドに値を割り当てます。 |
2 | リストが空の場合は、次の手順を実行してリストにノードを挿入します。 a) 新しいノードの次のフィールドが null を指すようにします。 b) 新しいノードの prev フィールドを null にポイントする; |
3 | リストの先頭にノードを挿入するには、次の手順を実行します。 a) 新しいノードの次のフィールドがリストの最初のノードを指すようにします。 b) start の prev フィールドに新しいノードを指定します。 c) 新しいノードの prev フィールドを null にポイントします。 d) 新しいノードの指定を開始します。 |
4 | 2 つの既存のノードの間に新しいノードを挿入するには、次の手順を実行します。 a) 新しいノードの次のノードが現在のノードを指すようにします。 b) 新しいノードの prev が前のノードを指すようにします。 c) 現在のノードの前のノードが新しいノードを指すようにします。 d) 前のノードの次のノードが新しいノードを指すようにします。 |
5 | 新しいノードをリストの末尾に挿入し、現在のポインタを最後のノードに移動する場合は、次の手順を実行します。 a) 現在のノードの次のノードに新しいノードを指定させます。 b) 新しいノードの prev が現在のノードを指すようにします。 c) 新しいノードの次を null にします。 |
3. 削除操作
二重リンクリスト内のノードを削除するための具体的なアルゴリズムは次のとおりです。
ステップ | 操作する |
---|---|
1 | 削除するノードを見つけて、削除するノードを現在のノードとしてマークします。 |
2 | 削除したノードが最初のノードの場合は、現在のノードの次のノードを直接開始点にします |
3 | 削除するノードが 2 つのノード間のノードである場合は、次の手順を実行します。 a) 前のノードの次のフィールドが現在のノードの次のノードを指すようにします。 b) 現在のノードの後のノードの prev フィールドが前のノードを指すようにします。 c) 現在のノードとしてマークされたノード メモリを解放します |
4 | 削除されたノードが最後のノードの場合は、削除されたノードが 2 つのノード間のノードである場合に、手順 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;
}
}
実装方法と原理
二重リンク リストを実装する場合は、ポインタの正確性やメモリ管理などの問題に注意する必要があります。パフォーマンスとスケーラビリティを向上させるために、センチネル ノードや循環リンク リストなどのテクノロジーを使用して二重リンク リストを最適化できます。
-
二重リンク リストは一般的なデータ構造です。単一リンク リストと比較すると、先行ノードへのポインタが追加されるため、各ノードは後続ノードへのポインタに加えて先行ノードへのポインタを持ちます。以下では、主に次の側面を含む二重リンクリストの実装方法と原理を紹介します。
-
ノード定義: 二重リンク リストの各ノードには 3 つの基本要素が含まれている必要があります。1 つはデータを格納するための変数で、他の 2 つは先行ノードと後続ノードへのポインターです。
-
ヘッド ノードとテール ノード: ヘッド ノードとテール ノードは、二重リンク リスト内の 2 つの特別なノードです。ヘッド ノードには先行ノードがなく、テール ノードには後続ノードがありません。
-
添加操作:向双向链表中添加新节点,需要创建一个新节点,并将其插入到链表的合适位置上,同时设置新节点的前驱和后继指针。
-
删除操作:从双向链表中删除节点,需要找到待删除节点的前驱节点和后继节点,然后修改它们的前驱和后继指针,使其不再指向待删除节点。
-
查找操作:查找双向链表中的某个节点,可以从链表的头节点或尾节点开始遍历整个链表,直到找到目标节点或遍历完整个链表。
-
遍历操作:遍历双向链表,可以从链表的头节点或尾节点开始,依次输出每个节点的数据。
-
长度统计:统计双向链表中节点的数量,可以通过遍历链表并计数的方式来实现。
了解单链表的实现方法和原理对于理解链表性能和优化有很大帮助。同时,需要注意在具体实现时要根据实际情况进行合理的设计和优化,以提高代码的效率和可维护性。
应用场景和使用注意事项
单链表是一种常见的数据结构,常用于以下应用场景:
-
实现栈和队列:单链表可以用来实现栈和队列。在栈中,元素只能从栈顶进出;在队列中,元素只能从队尾进,队首出。利用单链表的头插和尾插操作,可以很方便地实现这两种数据结构。
-
内存分配:在计算机内存管理中,单链表常被用作动态内存分配的数据结构。通过链表节点之间的指针连接,可以动态地申请和释放内存块。
-
音频和视频播放列表:单链表可以用来实现音频和视频播放列表。每个节点表示一个音频或视频文件,并保存下一个文件的位置。通过遍历链表,可以依次播放整个列表中的音频和视频。
-
寻找环形结构:单链表也可以用来处理关于环形结构的问题,例如判断一个链表是否有环、找到环的入口等。
-
缓存淘汰策略:在缓存系统中,当缓存空间已满时,需要淘汰一些数据来腾出空间。单链表可以用来维护缓存中的数据项,同时记录它们的使用情况。当需要淘汰数据时,可以选择最近最少使用的数据项进行淘汰,即删除单链表尾部的节点。
在使用单链表时需要注意以下几点:
-
空指针问题:在链表操作中容易出现空指针问题,例如访问空链表或者一个不存在的节点等。为了避免这些问题,需要对输入参数进行判空处理。
-
内存管理问题:在插入和删除节点时需要分配或释放内存空间,如果管理不当容易出现内存泄漏或者重复释放等问题。可以使用垃圾回收机制或手动管理内存空间来解决这些问题。
-
边界条件问题:在进行链表操作时需要处理一些边界条件和异常情况,例如链表为空、插入位置超出范围等情况,以确保链表的正常运行。
-
性能问题:在处理大规模数据时,单链表会存在一些性能问题,例如随机访问速度较慢、空间开销较大等。因此在实际应用中需要根据实际情况选择合适的数据结构。
算法和复杂度分析:
常见的双向链表算法包括插入、删除和查找操作,下面对它们进行简要分析:
-
插入操作:在双向链表中插入一个节点时,需要找到待插入位置的前驱节点和后继节点,并修改它们的指针。时间复杂度为O(n)。
-
删除操作:在双向链表中删除一个节点时,也需要找到待删除节点的前驱节点和后继节点,并修改它们的指针。时间复杂度为O(n)。
-
查找操作:在双向链表中查找一个节点时,可以从头节点或尾节点开始遍历整个链表,直到找到目标节点或遍历完整个链表。时间复杂度为O(n)。
-
排序算法:双向链表可以使用插入排序、冒泡排序等算法进行排序,排序的时间复杂度取决于具体算法实现。
另外,在实际应用中,可以通过使用哨兵节点、循环链表等技术来优化双向链表的性能和扩展性。
总之,双向链表是一种常见的数据结构,具有良好的灵活性和扩展性。在实际使用中,需要根据具体情况选择合适的算法,并注意内存管理和指针操作。
与其他数据结构的比较:
双向链表是一种常见的数据结构,与其他数据结构相比较有以下优点和缺点:
-
与数组比较:双向链表可以动态增加和删除节点,不需要预先分配固定大小的空间,因此具有更好的灵活性。但是,双向链表的访问时间复杂度为O(n),而数组的访问时间复杂度为O(1),因此在随机访问和遍历操作比较频繁的情况下,数组可能更加高效。
-
与队列比较:双向链表和队列都可以实现FIFO(先进先出)的数据结构,但是双向链表相比队列更加灵活,可以支持在任意位置插入和删除节点,而队列只能在头部插入和尾部删除元素。
-
与栈比较:双向链表和栈都可以实现FILO(先进后出)的数据结构,但是双向链表相比栈更加灵活,可以支持在任意位置插入和删除节点,而栈只能在栈顶插入和删除元素。
-
与哈希表比较:双向链表和哈希表都是常用的数据结构,但是它们的应用场景不同。哈希表适用于快速查找和插入键值对的场景,而双向链表适用于需要频繁插入和删除元素的场景。
总之,双向链表具有一些特点,如动态性、灵活性等优点,但是在访问效率等方面可能不如其他数据结构。在实际应用中,需要根据具体情况选择合适的数据结构,并进行合理的权衡和折衷。