[データ構造] 連結リストの単一連結リスト

目次

基本的な概念と操作:

実装方法と原理

利用シーンと使用上の注意点

アルゴリズムと複雑さの分析:

他のデータ構造との比較:


基本的な概念と操作:

単一リンク リストは、複数のノードで構成される一般的な線形データ構造であり、各ノードにはデータ要素と次のノードへのポインターの 2 つの部分が含まれます。各ノードには、通常は次ポインタと呼ばれる、ノードの後続ノードを指すポインタが 1 つだけあります。ヘッド ノードは、最初のノードの前に追加される追加のノードであり、データ要素は含まれませんが、最初のノードへのポインタが含まれます。

単一リンク リストは任意の長さのシーケンスを表すことができ、ノードを動的に挿入および削除できるため、柔軟かつ効率的になります。ただし、各ノードにはポインターが 1 つしかないため、単一リンクされたリストは前から後ろにのみ移動でき、逆に移動することはできません。

単一リンク リストのノードは、C# 言語で次のように記述されます。

namespace DataStructLibrary
{
    class SNode<T>
    {
        private T data;//数据域
        private SNode<T> next;//引用域
        public SNode(T val,SNode<T> p)
        {
            data = val;
            next = p;
        }

        public SNode(SNode<T> p)
        {
            next = p;

        }

        public SNode(T val)
        {
            data = val;
            next = null;
        }

        public SNode()
        {
            data = default(T);
            next = null;
        }

        public T Data
        {
            get { return data; }
            set { data = value; }
        }

        public SNode<T> Next
        {
            get { return next; }
            set { next = value; }
        }
    }
}

単一リンクリストに対して一般的に使用される操作は次のとおりです。

  1. リンク リストの作成: ヘッド ノードの有無にかかわらず、空のリンク リストを作成します。

ステップ 操作する
1 単一リンクリストの最初のノードを指すノードタイプの開始変数を宣言します。
2

単一リンクリストのコンストラクターで開始変数の値を null に代入します。

     2. ノードの挿入: リンクされたリストの指定された位置 (先頭、末尾、中央など) に新しいノードを挿入します。

         2.1 単一リンクリストの先頭に新しいノードを挿入する

        2.2 リンクリスト内の 2 つのノードの間にノードを挿入する

        2.3 リンクリストの最後に新しいノードを挿入する

単一リンク リストの最後にノードを挿入することは、リンク リストの 2 つのノードの間にノードを挿入する特殊なケースです。current が null で、previous が最後のノードを指している場合、新しいノードはリストの最後に挿入できます。リンクされたリスト。場合によっては、ノードがリンク リストの最後に挿入されることが明らかな場合は、次のアルゴリズム ステップを実行できます。

ステップ 操作する
1

新しいノードにメモリを割り当て、データフィールドに値を割り当てます

2

リスト内の最後のノードを見つけて、それを現在のノードとしてマークします

3

現在の次のフィールドが新しいノードを指すようにします

4

これはヌルを指すノットの次のフィールドであり、現在のスペースを解放します。

      3. 削除操作

      単一リンクリストから指定したノードを削除するには、まずリストが空かどうかを確認します。空でない場合は、指定したノードを最初に検索する必要があり、指定したノードが見つかった場合は削除されます。そうでない場合は、対応するノードが見つからないことを示すプロンプト メッセージが表示されます。削除されたノードが見つかると、指定されたノードが単一リンク リストから削除されます。通常、次の 3 つの状況に分けられます。

         3.1 単一リンクリストの先頭ノードの削除

ステップ 操作する
1

リストの最初のノードを現在のノードとしてマークします

2

start を使用して、単一リンクリスト内の次のノードをポイントします。

3

現在のノードとしてマークされたメモリを解放する

         3.2 単一リンクリスト内の 2 つのノード間のノードを削除する

         3.3 単一リンクリストの末尾ノードの削除

         一方向リンクリスト内の 2 つのノード間のノードを削除する上記のアルゴリズムでは、検索操作の後、現在のノードがリスト内の最後のノードを指している場合、削除されるノードがリスト内の最後のノードであることを意味します。リスト。このアルゴリズムは、一方向チェーン式の終わりにあるノードを削除することもできます。したがって、単一リンクリストの末尾にあるノードを削除するためのアルゴリズムを特別に作成する必要はありません。

     4. テーブル要素を取得し、要素を見つける

     テーブル要素のフェッチと要素の検索とは、指定されたシーケンス番号またはノード値に従って、シーケンス番号または値に対応するノードを検索することを指します。具体的なプロセスは次のとおりです。

 ステップ

操作する

1

単一リンクリストの開始ノードを現在のノードとしてマークします。

2

単一のリンク リストが空のリンク リストではない場合は、検索対象のシリアル番号または値が、現在の参照が指すシリアル番号または値と等しいかどうかを比較し、そうでない場合は、現在の参照が次のノードを指すかどうかを比較します。ノードが見つかったので、現在を返します

3

current が null の場合、指定されたノードが見つからなかったことを意味します

      5. 逆リンクリスト: リンクリスト内のノードを逆順に配置します。

      6. リンク リストの追加: 別のリンク リストの先頭をこのリンク リストに追加します。

上記の操作は、単一リンク リストの最も基本的な操作であり、これを通じてスタック、キュー、ハッシュ テーブルなどの他の多くのデータ構造を実装できます。リンク・リストの正常な動作を保証するために、リンク・リストの操作を実行する際には、いくつかの境界条件や異常な条件に対処する必要があることに注意してください。

テーブルの長さの計算、空であるかどうかの判断など、線形テーブルに関連するその他の操作は、シーケンシャル テーブルでの実装が比較的簡単です。単一リンク リストについては、次の C# コードを参照してください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DataStructLibrary
{
    class SingleLinkList<T>:ILinarList<T>
    {
        public SNode<T> start;//单链表的头引用
        int length;//单链表长度
        /// <summary>
        /// 初始化单链表
        /// </summary>
        public SingleLinkList()
        {
            start = null;
        }


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

        /// <summary>
        /// 在单链表的第i个数据元素的位置前插入一个数据元素data
        /// </summary>
        /// <param name="data"></param>
        /// <param name="i"></param>
        public void InsertNode(T data, int i)
        {
            SNode<T> current;
            SNode<T> previous;
            if(i<1 || i> (length + 1))
            {
                Console.WriteLine("Position is error!");
                return;
            }
            SNode<T> newNode = new SNode<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)
            {
                previous.Next = newNode;
                newNode.Next = current;
                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");
            }
            SNode<T> current = start;
            if(i == 1)
            {
                start = current.Next;
                length--;
                return;
            }
            SNode<T> previus = null;
            int j = 1;
            while(current.Next != null && j<i)
            {
                previus = current;
                current = current.Next;
                j++;
            }
            if (j == i)
            {
                previus.Next = current.Next;
                current = current.Next;
                length--;
                return;
            }
            //第i个节点不存在
            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);
            }
            SNode<T> current = start;
            int j = 1;
            while (current.Next != null && j<i)
            {
                current = current.Next;
                j++;
            }
            if (j == i)
            {
                return current.Data;
            }
            //第i个节点不存在
            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);
            }
            SNode<T> current = start;
            int i = 1;
            while(current != null && !current.Data.ToString().Contains(data.ToString()))
            {
                current = current.Next;
                i++;
            }
            if(current != null)
            {
                return current.Data;
            }
            return default(T);
        }

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

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



        /// <summary>
        /// 将另外一个链表头部追加到本链表
        ///  /// 例如链表头结点start:由原来的
        /// data:a,
        /// next:[
        ///      data:b,
        ///      next:[
        ///           data:c,
        ///           next:null
        ///          ]
        ///     ] 
        ///翻转后的结果为:
        /// data:a,
        /// next:[
        ///      data:b,
        ///      next:[
        ///           data:c,
        ///           next:[
        ///                 data:x,
        ///                 next:[
        ///                       data:y,
        ///                       next:[
        ///                             data:z,
        ///                             next:null
        ///                            ]
        ///              
        ///                      ]
        ///               ]
        ///          ]
        ///     ] 
        /// </summary>
        /// <param name="nodeB">
        /// 追加的单链表头部数据
        /// 例如:
        /// data:x,
        /// next:[
        ///      data:y,
        ///      next:[
        ///           data:z,
        ///           next:null
        ///          ]
        ///     ]
        /// </param>
        public void Append(SNode<T> nodeB)
        {
            if (nodeB == null)
            {
                return;
            }
            InsertNode(nodeB.Data);
            SNode<T> tmpNode = nodeB.Next;
            while (tmpNode != null)
            {
                InsertNode(tmpNode.Data);
                tmpNode = tmpNode.Next;
            }

            return;
        }

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

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

    }
}

実装方法と原理

単一リンクリストの実装方法と原理には、主に次の側面が含まれます。

  1. ノード: 単一リンク リストのノードは、データ フィールドとポインター フィールドの 2 つの部分で構成されます。データ フィールドにはノードのデータ要素が格納され、ポインタ フィールドは次のノードを指します。ノードは構造体で表すことができます。

  2. ヘッド ノード: ヘッド ノードは、リンク リストの最初のノードの前に追加される追加のノードであり、データ要素は含まれませんが、最初のノードへのポインタが含まれます。ヘッド ノードの役割は、リンク リストの操作を簡素化し、挿入や削除などの操作をより便利にすることです。

  3. ポインタ操作: リンクされたリスト内のノード間の接続は、ポインタを介して実現されます。各ノードには次のノードを指すポインターがあり、ノードを挿入または削除するときにノード間のポインター関係を変更する必要があります。

  4. リンク リストの走査: リンク リストの走査とは、リンク リスト内のすべてのノードを順番に訪問することを指します。リンク リストをトラバースすることで、ノード内のデータ値を読み取りまたは変更できます。また、リンク リストの長さを計算して特定のノードを見つけることもできます。

  5. メモリ管理: ノードの挿入および削除時にメモリ空間を割り当てまたは解放する必要があります。メモリ リークや繰り返し解放などの問題を回避するには、メモリ空間を合理的に管理する必要があります。

  6. 境界条件の処理: リンク リストの正常な動作を保証するために、リンク リストの操作を実行するときに、リンク リストが空である、挿入位置が範囲外であるなど、一部の境界条件や異常な条件に対処する必要があります。

単一リンク リストの実装方法と原理を理解することは、リンク リストのパフォーマンスと最適化を理解するのに非常に役立ちます。同時に、コードの効率と保守性を向上させるために、特定の実装では、実際の状況に応じた合理的な設計と最適化に注意を払う必要があります。

利用シーンと使用上の注意点

単一リンク リストは一般的なデータ構造であり、次のアプリケーション シナリオでよく使用されます。

  1. スタックとキューの実装: 単一リンク リストを使用してスタックとキューを実装できます。スタックでは、要素はスタックの先頭からのみ出入りできますが、キューでは、要素はキューの最後から出入りし、キューの先頭からのみ出ることができます。これら 2 つのデータ構造は、単結合リストの先頭挿入操作と末尾挿入操作を使用することで簡単に実現できます。

  2. メモリ割り当て: コンピュータのメモリ管理では、単一リンク リストが動的メモリ割り当てのデータ構造としてよく使用されます。リンクされたリストのノード間のポインタ接続を通じて、メモリ ブロックを動的に割り当てたり解放したりできます。

  3. オーディオおよびビデオのプレイリスト: 単一リンクされたリストを使用して、オーディオおよびビデオのプレイリストを実装できます。各ノードはオーディオまたはビデオ ファイルを表し、次のファイルの位置を保持します。リンクされたリストをたどることにより、リスト全体のオーディオとビデオを順番に再生できます。

  4. 循環構造の検索: 単一リンク リストは、リンク リストにリングがあるかどうかの判断、リングのエントリの検索など、循環構造に関連する問題を処理するためにも使用できます。

  5. キャッシュ削除戦略: キャッシュ システムでは、キャッシュ スペースがいっぱいになると、領域を空けるために一部のデータを削除する必要があります。単一リンク リストを使用すると、使用状況を記録しながらキャッシュ内のデータ項目を維持できます。データを削除する必要がある場合、最も最近使用されていないデータ項目を選択して削除できます。つまり、単一リンクリストの末尾のノードが削除されます。

単一リンクリストを使用する場合は、次の点に注意する必要があります。

  1. Null ポインターの問題: Null ポインターの問題は、空のリンク リストや存在しないノードへのアクセスなど、リンク リスト操作で発生する傾向があります。これらの問題を回避するには、入力パラメータに対してnull判定を行う必要があります。

  2. メモリ管理の問題: ノードの挿入や削除の際にはメモリ領域の割り当てや解放が必要ですが、管理が適切に行われていない場合、メモリ リークや解放の繰り返しなどの問題が発生する可能性があります。これらの問題は、ガベージ コレクション メカニズムを使用するか、メモリ領域を手動で管理することで解決できます。

  3. 境界条件の問題: リンク リストの正常な動作を保証するには、リンク リストが空である、挿入位置が範囲外であるなど、リンク リスト操作を実行するときに一部の境界条件や異常な条件に対処する必要があります。

  4. パフォーマンスの問題: 大規模なデータを扱う場合、単一リンク リストには、ランダム アクセス速度の遅さやスペース オーバーヘッドの多さなど、パフォーマンスの問題が発生する可能性があります。したがって、実際のアプリケーションでは、実際の状況に応じて適切なデータ構造を選択する必要があります。

アルゴリズムと複雑さの分析:

  1. リンクリストにリングがあるかどうかを判断する方法、リンクリストを反転する方法、2 つの順序付けされたリンクリストをマージする方法など、単一リンクリストのアルゴリズムと複雑さの分析を理解します。これらのアルゴリズムをマスターすると、単一リンク リストについての理解が深まると同時に、プログラミング スキルも向上します。

単結合リスト アルゴリズムと複雑さの分析には、主に次の側面が含まれます。

  1. リンク リストの走査: リンク リストの走査は、リンク リスト内のすべてのノードにアクセスする基本操作であり、ループまたは再帰を通じて実装できます。時間計算量は O(n) です。ここで、n はリンク リストの長さです。

  2. 指定したノードを検索: リンクされたリストで指定したノードを検索します。ノードの位置またはノードのキーワードに従って検索できます。線形探索の時間計算量は O(n)、二分探索の時間計算量は O(logn) です。

  3. ノードの挿入: リンク リストに新しいノードを挿入するには、ノード間のポインタ関係を変更する必要があります。時間計算量は、最初または最後に挿入する場合は O(1)、それ以外の場合は O(n) です。

  4. ノードの削除: リンク リスト内のノードを削除するには、ノード間のポインタ関係を変更する必要があります。削除がヘッド ノードまたはテール ノードの場合、時間計算量は O(1)、それ以外の場合は O(n) です。

  5. 逆リンク リスト: リンク リスト内のノードを逆順に配置するには、ノード間のポインタ関係を変更する必要があります。反復または再帰を使用して実装でき、時間計算量は O(n) です。

  6. リンク リストのマージ: 2 つの順序付きリンク リストを新しい順序付きリンク リストにマージするには、2 つのリンク リストのノードを 1 つずつ比較し、それらをマージする必要があります。時間計算量は O(m+n) です。ここで、m と n は 2 つのリンクされたリストの長さをそれぞれ表します。

連結リストアルゴリズムを実行する際には、連結リストが空である、挿入位置が範囲外であるなど、いくつかの境界条件や異常条件への対処に注意する必要があることに注意してください。同時に、実際の実装では、コードの効率と保守性を向上させるために、実際の状況に応じて合理的な設計と最適化を実行する必要があります。

他のデータ構造との比較:

  1. 単一リンク リストが他のデータ構造 (配列、二重リンク リストなど) とどのように比較されるかを学びます。それらの長所と短所、および適用可能なシナリオを理解することで、実際的な問題を解決するために適切なデータ構造をより適切に選択できるようになります。

単一リンク リストと他のデータ構造の比較には、主に次の側面が含まれます。

  1. 配列: 配列と単一リンク リストの両方を使用してシーケンスを表すことができますが、実装方法は異なります。配列はメモリ内の記憶領域の連続的な割り当てであり、任意の要素に直接アクセスできます。単一リンクリストはポインターを介してノードを接続する必要があり、前から後ろへのみ移動できます。単一リンクリストの各ノードにはポインターが 1 つだけあるため、スペースのオーバーヘッドは小さいですが、配列には一定サイズのメモリスペースを事前に割り当てる必要があり、要素の挿入または削除時に一部のスペースが無駄になる可能性があります。

  2. スタックとキュー: スタックとキューは、配列または単一リンク リストを使用して実装できる 2 つの一般的なデータ構造です。配列で実装されたスタックとキューにはランダムにアクセスできますが、要素を挿入または削除するときに他の要素を移動する必要があり、時間計算量は O(n) です。単結合リストで実装されたスタックとキューは、先頭または末尾で挿入と削除操作を実行でき、時間計算量は O(1) です。

  3. ハッシュ テーブル: ハッシュ テーブルはハッシュ関数に基づくデータ構造であり、要素を迅速に検索して変更できます。ハッシュ テーブルの実装では、通常、要素を格納するために配列が使用され、ハッシュの衝突を処理するためにリンク リストまたはその他のデータ構造が使用されます。単一リンク リストは、ハッシュ テーブルの連鎖ストレージの実装として使用できますが、前から後ろへのみトラバースできるため、ハッシュ テーブルのクエリ効率に影響を与える可能性があります。

  4. 赤黒ツリー: 赤黒ツリーは自己平衡型二分探索ツリーであり、高速な挿入、削除、検索操作を実現できます。単一リンク リストなどのデータ構造と比較して、赤黒ツリーは時間の複雑さが低く、範囲検索やランキング操作などの高度な機能をサポートします。しかし、それに応じて、実装もより複雑になり、さまざまな回転操作や色付け操作を処理する必要があります。

要約すると、単一リンク リストはシーケンスを表すのに適しており、スペースのオーバーヘッドが少なく、動的な挿入および削除操作をサポートします。配列と比較すると、要素の挿入と削除の時間計算量は低く、スタックやキューなどの他のデータ構造と比較すると、挿入と削除の操作を途中で実行する方が便利です。ただし、クエリや並べ替えなどの操作のパフォーマンスは低く、赤黒ツリーなどの高度なデータ構造ほど優れていません。

PS: 間違いや脱落があれば、修正してください

おすすめ

転載: blog.csdn.net/beenles/article/details/131110584