データ構造の復習: 7 日目

目次

1. 循環リンクリスト

 1. ハッシュテーブル

アイデアとアルゴリズム

複雑さの分析

2.スピードポインター

アイデアとアルゴリズム

詳細

複雑さの分析

2. 2 つの順序付きリンク リストを結合する

 1. 再帰

アルゴリズム

複雑さの分析

2. 反復

アイデア

アルゴリズム

複雑さの分析

3. リンクされたリスト要素を削除する

1. 再帰

複雑さの分析

2. 反復

複雑さの分析

1. 循環リンクリスト

141. リングリンクリスト - LeetCode https://leetcode.cn/problems/linked-list-cycle/?plan=data-structions&plan_progress=ggfacv7

 1. ハッシュテーブル

アイデアとアルゴリズム

最も簡単に考える方法は、すべてのノードを走査し、ノードを走査するたびに、そのノードが以前に訪問されたことがあるかどうかを判断することです。

具体的には、ハッシュ テーブルを使用して、訪問したすべてのノードを保存できます。ノードに到達するたびに、そのノードがハッシュ テーブルに既に存在する場合、リンク リストは循環リンク リストになります。それ以外の場合、ノードはハッシュ テーブルに追加されます。リンクされたリスト全体を調べ終わるまで、このプロセスを繰り返します。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head != nullptr) {
            if (seen.count(head)) {
                return true;
            }
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};

複雑さの分析

時間計算量: O(N)、N はリンク リスト内のノードの数です。最悪の場合、各ノードを 1 回通過する必要があります。

空間複雑度: O(N)、N はリンク リスト内のノードの数です。主にハッシュ テーブルのオーバーヘッドが原因で、最悪の場合、各ノードをハッシュ テーブルに 1 回挿入する必要があります。

2.スピードポインター

アイデアとアルゴリズム

この方法を使用するには、読者が「フロイドのサークル アルゴリズム」 (ウサギとカメのアルゴリズムとしても知られる) についてある程度の理解を必要とします。

「カメ」と「ウサギ」がリンクされたリスト上を移動していると想像してください。「ウサギ」は速く走り、「カメ」はゆっくりと走ります。「カメ」と「ウサギ」がリンク リストの同じノードから移動を開始するとき、リンク リストにサイクルがない場合は、「ウサギ」が常に「カメ」の前になります。リンクリスト、次に「ウサギ」 「カメ」より先にリングに入り、リング内を移動し続けます。「カメ」がリングに入ると、「ウサギ」のスピードが速いため、必ずどこかの時点でカメに遭遇します。つまり、「カメ」を何度かトラップします。

上記の考え方に基づいてこの問題を解決できます。具体的には、速いポインターと遅いポインターの 2 つのポインターを定義します。低速ポインタは一度に 1 ステップのみ移動しますが、高速ポインタは一度に 2 ステップ移動します。最初は、低速ポインタは位置 head にあり、高速ポインタは位置 head.next にあります。このように、移動中に速いポインタが遅いポインタに追いついた場合、そのリンクリストは循環リンクリストであることを意味する。そうしないと、高速ポインタがリンク リストの最後に到達し、リンク リストは循環リンク リストではなくなります。

詳細

なぜ両方のポインタが head の位置にあるのではなく、遅いポインタが最初は head の位置にあり、高速ポインタが head.next の位置にあると規定する必要があるのでしょうか (つまり、「Turtle」と「Rabbit」の場合と同じ説明です)。 )?

次のコードに注目してください。while ループを使用しており、ループ条件がループ本体の前にあります。ループ条件は、高速ポインタと低速ポインタが重複するかどうかを判断する必要があるため、最初に両方のポインタを先頭に配置すると、while ループは実行されません。したがって、head の前に仮想ノードがあり、低速ポインタは仮想ノードから head.next まで 1 ステップ移動し、高速ポインタは仮想ノードから head.next まで 2 ステップ移動することを想定して、while ループを使用できます。

もちろん、do-while ループも使用できます。この時点で、高速ポインタと低速ポインタの初期値を head に設定できます。

class Solution {
public:
    bool hasCycle(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return false;
        }
        ListNode* slow = head;
        ListNode* fast = head->next;
        while (slow != fast) {
            if (fast == nullptr || fast->next == nullptr) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }
        return true;
    }
};

複雑さの分析

時間計算量: O(N)、N はリンク リスト内のノードの数です。

リンク リストにリングがない場合、高速ポインタは低速ポインタよりも先にリンク リストの末尾に到達し、リンク リストの各ノードは最大 2 回アクセスされます。

リンクされたリストにリングがある場合、高速ポインタと低速ポインタの間の距離は、移動のたびに 1 ずつ減少します。初期距離はリングの長さなので、最大で N ラウンド移動します。

空間複雑度: O(1)。2 つのポインターの余分なスペースのみを使用します。

2. 2 つの順序付きリンク リストを結合する

21. 2 つの順序付きリンク リストをマージする - LeetCode https://leetcode.cn/problems/merge-two-sorted-lists/?plan=data-structions&plan_progress=ggfacv7

 1. 再帰

次のように、2 つのリンク リストのマージ操作を再帰的に定義できます
(空のリンク リストなどの特殊なケースは無視します)。


list1[0] + merge(list1[1:],list2) list1[0] < list2[0]
list2[0] + merge(list1, list2[1:) それ以外の場合
、つまりヘッダー値2 つのリンクされたリストのうち、小さい方のノードが残りの要素のマージ操作の結果とマージされます。

アルゴリズム


上記の再帰プロセスを直接モデル化しますが、境界ケースを考慮する必要があります。
11 または 12 が最初から空のリンク リストである場合、操作をマージする必要はないため、空ではないリンク リストを返すだけで済みます。それ以外の場合は、リンク リストのヘッド ノードのどちらが小さい値 (11 または 12) を持つかを判断し、結果に追加する次のノードを再帰的に決定する必要があります。2 つのリンクされたリストのうちの 1 つが空の場合、再帰は終了します。
 

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == nullptr) {
            return l2;
        } else if (l2 == nullptr) {
            return l1;
        } else if (l1->val < l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        } else {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
    }
};

複雑さの分析


●時間計算量: O(n+m)、n と m はそれぞれ 2 つのリンクされたリストの長さです。再帰呼び出しごとにヘッド ノード 11 または 12 が削除されるため
(少なくとも 1 つのリンク リストが空になるまで)、関数 mergeTwoList は各ノードを最大 1 回再帰的に呼び出すだけです。したがって、時間計算量はマージされたリンク リストの長さ (O(n + m)) に依存します。
●空間計算量: O(n+n)、n と 1 はそれぞれ 2 つのリンクされたリストの長さです。mergeTwoLists 関数を再帰的に呼び出すにはスタック スペースが必要です。スタック スペースのサイズは再帰呼び出しの深さによって異なります。再帰呼び出しを終了するとき、mergeTwoLists 関数は
最大で n+m 回呼び出すことができるため、空間計算量は O(n + m) になります。
 

2. 反復

アイデア

反復法を使用して上記のアルゴリズムを実装できます。l1 と l2 のどちらも空のリンク リストでない場合は、どちらのリンク リストの先頭ノードの値が小さいかを判断し、値が小さい方のノードを結果に追加します。ノードが結果に追加されたら、対応するリンク リストをリストに追加します。結果、ノードが 1 つ後ろに移動します。

アルゴリズム

まず、センチネル ノードのプリヘッドを設定します。これにより、最後にマージされたリンク リストをより簡単に返すことができます。prev ポインターを維持します。必要なのは、その next ポインターを調整することです。次に、l1 または l2 が null を指すまで次のプロセスを繰り返します。 l1 の現在のノードの値が l2 以下の場合、l1 の現在のノードを前のノードの後ろに接続し、l1 を移動します。ポインターを 1 ビット戻します。それ以外の場合は、l2 に対して同じことを行います。どの要素を後ろに接続する場合でも、前に 1 つ後ろに移動する必要があります。

ループが終了すると、l1 と l2 の少なくとも 1 つは空ではありません。2 つの入力リンク リストは両方とも順序付けされているため、どちらのリンク リストが空でなくても、そこに含まれるすべての要素は、以前にマージされたリンク リストのすべての要素よりも大きくなります。これは、単純に空ではないリンク リストをマージされたリストに追加し、マージされたリストを返すことを意味します。

以下の図は、2 つのリンク リスト 1->4->5 および 1->2->3->6 を反復的にマージするプロセスを示しています。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* preHead = new ListNode(-1);

        ListNode* prev = preHead;
        while (l1 != nullptr && l2 != nullptr) {
            if (l1->val < l2->val) {
                prev->next = l1;
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next;
            }
            prev = prev->next;
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev->next = l1 == nullptr ? l2 : l1;

        return preHead->next;
    }
};

複雑さの分析

時間計算量: O(n+m)、n と m はそれぞれ 2 つのリンクされたリストの長さです。各ループ反復では、l1 と l2 の 1 つの要素だけがマージされたリンク リストに追加されるため、while ループの数が 2 つのリンク リストの長さの合計を超えることはありません。他のすべての操作の時間計算量は一定であるため、合計時間計算量は O(n+m) になります。

空間複雑度: O(1)。必要なのは、複数の変数を保存するための一定のスペースだけです。

3. リンクされたリスト要素を削除する

203. リンク リスト要素の削除 - LeetCode https://leetcode.cn/problems/remove-linked-list-elements/?plan=data-structions&plan_progress=ggfacv7

1. 再帰

リンク リストの定義は再帰的であるため、リンク リストの問題は多くの場合、再帰的手法を使用して解決できます。この質問では、ノード値が特定の値に等しいリンク リスト内のすべてのノードを削除する必要があります。これは再帰を使用して実現できます。

指定されたリンク リストについて、最初にヘッド ノード head を除くノードを削除し、次に head のノード値が指定された val に等しいかどうかを判断します。head のノード値が val に等しい場合、head を削除する必要があるため、削除操作後のヘッド ノードは head.next になります。head のノード値が val に等しくない場合、head は保持されるため、ヘッド ノードは削除操作後もまだ先頭です。上記の処理は再帰的な処理です。

再帰の終了条件は head が空であることです。この場合は head が直接返されます。head が空でない場合は、削除操作を再帰的に実行し、head のノード値が val に等しいかどうかを判断して、head を削除するかどうかを決定します。

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (head == nullptr) {
            return head;
        }
        head->next = removeElements(head->next, val);
        return head->val == val ? head->next : head;
    }
};

複雑さの分析

時間計算量: O(n)、n はリンク リストの長さです。リンクされたリストは、再帰プロセス中に 1 回スキャンする必要があります。

空間計算量: O(n)、n はリンク リストの長さです。空間の複雑さは主に再帰呼び出しスタックに依存し、最大でも n レベルを超えることはありません。

2. 反復

反復を使用して、ノード値が特定の値と等しいリンク リスト内のすべてのノードを削除することもできます。
現在のノードを表すには temp を使用します。temp の次のノードが空ではなく、次のノードのノード値が指定された val に等しい場合、
次のノードを削除する必要があります。次のノードの削除は次のように実行できます。


temp.next = temp.next.next temp
次のノードのノード値が指定された val と等しくない場合は、次のノードを保持し、temp を次のノードに移動します
temp の次のノードが空になると、リンク リストの走査が終了し、ノード値が val に等しいすべてのノードが削除されます。具体的な実装では、リンク リストのヘッド ノード head を削除する必要がある場合があるため、ダミー ノード dummyHead を作成し、
dummyHead.next= head を設定し、temp =dummyHead を初期化し、リンク リストを走査して削除操作を実行します。 。最後に
返される dummyHead.next は、削除操作後のヘッド ノードです。

 

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        struct ListNode* dummyHead = new ListNode(0, head);
        struct ListNode* temp = dummyHead;
        while (temp->next != NULL) {
            if (temp->next->val == val) {
                temp->next = temp->next->next;
            } else {
                temp = temp->next;
            }
        }
        return dummyHead->next;
    }
};

複雑さの分析

  • 時間計算量: O(n)、n はリンク リストの長さです。リンクされたリストを一度走査する必要があります。

  • 空間複雑度: O(1)。

おすすめ

転載: blog.csdn.net/m0_63309778/article/details/126743391