アルゴリズムレビューの 5 日目: ダブルポインター - 4

目次

1. リンクリストの中間ノード

 1. 配列:

アイデアとアルゴリズム

複雑さの分析

2. シングルポインタ方式

複雑さの分析

3. 高速ポインタ方式と低速ポインタ方式: (奇妙で狡猾)

アイデアとアルゴリズム

複雑さの分析

2. リンクリストの最後から 2 番目の N ノードを削除します。

序文

1. リンクされたリストの長さを計算します。

アイデアとアルゴリズム

複雑さの分析

2. 積み重ねる

アイデアとアルゴリズム

複雑さの分析

3. ダブルポインタ(高速ポインタと低速ポインタ)

アイデアとアルゴリズム

複雑さの分析

1. リンクリストの中間ノード

876. リンクされたリストの真ん中 - LeetCode https://leetcode.cn/problems/middle-of-the-linked-list/

 1. 配列:

アイデアとアルゴリズム


リンク リストの欠点は、添字を介して対応する要素にアクセスできないことです。したがって、リンクされたリストを走査し、走査された要素を配列 A に順番に入れることを検討できます。N 個の素数までトラバースすると、リンクされたリストと配列の長さも N になり、対応する中間ノードは A[N/2] になります。
 

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        vector<ListNode*> A = {head};
        while (A.back()->next != NULL)
            A.push_back(A.back()->next);
        return A[A.size() / 2];
    }
};

複雑さの分析

  • 時間計算量: O(N)、NN は指定されたリンク リスト内のノードの数です。

  • スペースの複雑さ: O(N)、つまり A 配列によって使用されるスペース。

2. シングルポインタ方式

方法 1 でスペースの最適化を実行し、配列 A を省略できます。
リンクされたリストを 2 回たどることができます。最初のトラバーサルでは、リンク リスト内の要素の数 N をカウントします。2 回目のトラバーサルでは
、 N/2 番目の要素 (リンク リストの最初のノードは 0 番目の要素) までトラバースすると、その要素が返されます。できる。
 

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        int n = 0;
        ListNode* cur = head;
        while (cur != nullptr) {
            ++n;
            cur = cur->next;
        }
        int k = 0;
        cur = head;
        while (k < n / 2) {
            ++k;
            cur = cur->next;
        }
        return cur;
    }
};

複雑さの分析

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

  • スペースの複雑さ: O(1)、変数とポインターを格納するために必要なのは定数スペースのみです。

3. 高速ポインタ方式と低速ポインタ方式: (奇妙で狡猾)

アイデアとアルゴリズム

2 つのポインターを低速と高速で使用して、リンク リストを一緒に走査して、方法 2 の最適化を続けることができます。遅いとは一度に 1 歩ずつ進むことを意味し、速いとは一度に 2 歩ずつ進むことを意味します。次に、fast がリンク リストの最後に達すると、slow が中央になければなりません。

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

複雑さの分析

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

スペースの複雑さ: O(1)。低速と高速の 2 つのポインターを格納するには一定のスペースのみが必要です。

2. リンクリストの最後から 2 番目の N ノードを削除します。

19. リンク リストの最後から N 番目のノードを削除します - LeetCode https://leetcode.cn/problems/remove-nth-node-from-end-of-list/

序文


リンク リストを操作する場合、一般的な手法は、 \textit{next}next ポインタがリンク リストのヘッド ノードを指すダミー ノードを追加することです。このように、ヘッド ノードに対して特別な判断を行う必要はありません。

たとえば、この質問では、ノード y を削除したい場合、ノード y の先行ノード x を知り、x のポインタを y の後続ノードに指す必要があります。ただし、ヘッド ノードには先行ノードがないため、ヘッド ノードを削除する場合は特別な判断が必要です。ただし、ダム ノードを追加すると、ヘッド ノードの先行ノードはダム ノードそのものになるため、この時点では一般的な状況のみを考慮する必要があります。

特に、一部の言語では、メモリを独自に管理する必要があります。したがって、実際の面接では面接官と積極的にコミュニケーションをとり、「削除したノードに対応する領域を解放する必要があるかどうか」について合意を得る必要があります。次のコードは、デフォルトではスペースを解放しません。

1. リンクされたリストの長さを計算します。

アイデアとアルゴリズム

簡単に考えると、まずヘッド ノードから開始してリンク リストを走査し、リンク リストの長さ L を取得します。次に、ヘッド ノードから開始してリンク リストをたどって、L-n+1 番目のノードまでたどると、それが削除する必要があるノードになります。

質問の n と矛盾しないように、ノードの番号は 1 から始まり、先頭ノードは番号 1 のノードになります。

削除操作を容易にするために、ダム ノードから開始して L-n+1 ノードをトラバースできます。L−n+1 番目のノードがトラバースされると、その次のノードが削除する必要があるノードになるため、削除操作を完了するにはポインタを 1 回変更するだけで済みます。

class Solution {
public:
    int getLength(ListNode* head) {
        int length = 0;
        while (head) {
            ++length;
            head = head->next;
        }
        return length;
    }

    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode* cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur->next;
        }
        cur->next = cur->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

複雑さの分析

  • 時間計算量: O(L)、LL はリンク リストの長さです。

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

2. 積み重ねる

アイデアとアルゴリズム

リンクされたリストを走査しながら、すべてのノードを順番にスタックにプッシュすることもできます。スタックの「先入れ後出し」の原則に従って、スタックから取り出した n 番目のノードが削除する必要があるノードであり、現在スタックの一番上にあるノードが削除の先行ノードです。削除するノード。これにより、削除操作が非常に便利になります。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        stack<ListNode*> stk;
        ListNode* cur = dummy;
        while (cur) {
            stk.push(cur);
            cur = cur->next;
        }
        for (int i = 0; i < n; ++i) {
            stk.pop();
        }
        ListNode* prev = stk.top();
        prev->next = prev->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

複雑さの分析

  • 時間計算量: O(L)、LL はリンク リストの長さです。

  • 空間計算量: O(L)、LL はリンク リストの長さです。主にスタックのオーバーヘッドが原因です。

3. ダブルポインタ(高速ポインタと低速ポインタ)

アイデアとアルゴリズム

リンク リストの長さを前処理したり、定数スペースを使用したりせずに、この問題を解決することもできます。

下から n 番目のノードを見つける必要があるため、最初と 2 番目の 2 つのポインターを使用してリンク リストを同時に走査できます。最初のポインターは 2 番目のノードよりも n ノード前にあります。最初にリンク リストの最後まで移動すると、2 番目は下からちょうど n 番目のノードになります。

具体的には、最初は 1 番目と 2 番目の両方がヘッド ノードを指します。まず first を使用してリンク リストを走査します。走査回数は n です。このとき、1 番目と 2 番目の間には n-1 個のノードが存在します。つまり、1 番目のノードは 2 番目のノードよりも n 個前にあります。

その後、first と Second を同時に使用して、リンクされたリストを走査します。first がリンク リストの最後まで移動するとき (つまり、first が null ポインターである場合)、 Second は下から n 番目のノードを指すだけです。

方法 1 と方法 2 に従って、下から n 番目のノードではなく、下から n 番目のノードの先行ノードを取得できれば、削除操作がより便利になります。したがって、最初は 2 番目にダム ノードを指定することを検討でき、残りの操作ステップは変更されません。このようにして、最初にリンク リストの最後まで移動すると、2 番目の次のノードが削除する必要があるノードになります。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        ListNode* first = head;
        ListNode* second = dummy;
        for (int i = 0; i < n; ++i) {
            first = first->next;
        }
        while (first) {
            first = first->next;
            second = second->next;
        }
        second->next = second->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

複雑さの分析

  • 時間計算量: O(L)、L はリンク リストの長さです。

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

おすすめ

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