コードのランダム記録 - リンクされたリストの概要

リンクリストの理論的基礎

単一リスト

単一リンク リストは、一連のノードで構成される一般的な線形データ構造であり、各ノードにはデータ要素と次のノードへのポインターが含まれます。単結合リストの最初のノードはヘッド ノードと呼ばれ、最後のノードはテール ノードと呼ばれ、テール ノードのポインタはヌル アドレスを指します。

単一リンク リストの利点は、挿入と削除の操作は時間計算量が O(1) で比較的高速であるのに対し、検索操作はリンク リスト全体を走査する必要があり、時間計算量は O(n) であることです。

単一リスト

コードは以下のように表示されます。

struct ListNode {
    
    
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {
    
    }  // 节点的构造函数(C++11的初始化列表(initializer list)语法)
};

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

  • ノードを挿入および削除するときは、リンク リスト内の隣接するノードのポインタの更新に注意してください。
  • ノードを使用した後、メモリ リークを避けるために、ノードが占有しているメモリを手動で解放する必要があります。
  • リンク リストをトラバースするときは、ポインタが null アドレスを指すまでループ構造を使用する必要があります。

二重リンクリスト

二重リンクリストの各ノードには、データ要素と、前のノードと次のノードをそれぞれ指す 2 つのポインターが含まれています。単一リンク リストと比較して、二重リンク リストには前のノードを指す追加のポインタがあり、双方向のトラバーサル操作やノードの挿入および削除操作の実行が容易になります。

C++ では、次の構造を使用して二重リンク リストのノードを定義できます。

struct ListNode {
    
    
    int val;              // 存储的元素值
    ListNode* prev;       // 指向前一个节点的指针
    ListNode* next;       // 指向后一个节点的指针
    ListNode(int x) : val(x), prev(nullptr), next(nullptr) {
    
    }  // 构造函数
};

循環リンクリスト

循環リンク リストは、最後のノードの次のノードが最初のノードを指し、リングを形成する特殊なリンク リストです。循環リンク リストは、一方向循環リンク リストと双方向循環リンク リストの 2 つのタイプに分類できます。

一方向循環リンク リストでは、各ノードには次のノードを指すポインターが 1 つだけあります。最後のノードのポインタは最初のノードを指します。

二重リンクリストでは、各ノードには 2 つのポインターがあり、1 つは前のノードを指し、もう 1 つは次のノードを指します。最初のノードの前のノードは最後のノードを指し、最後のノードの後のノードは最初のノードを指します。

循環リンク リストの利点は、最後のノードから最初のノードに戻ることができるため、循環トラバーサルを簡単に実装できることです。同時に、循環リンク リストはリンク リストの「テール ポインタ」問題を解決できます。つまり、リンク リストの最後にノードを追加するときに、ノードを見つけるためにリンク リスト全体をたどる必要がありません。最後のノード。

ただし、循環リンク リストの欠点は、実装がより複雑で、最後のノードのポインタの特別な処理が必要になることです。同時に、循環リンク リスト内のノードの数は制御が難しく、メモリ リークや無限ループなどの問題が発生しやすくなります。

一方向循環リンク リストの構造は次のとおりです。

struct ListNode {
    
    
    int val;
    struct ListNode *next;
};

struct CircularLinkedList {
    
    
    ListNode *head;
    ListNode *tail;
    int size;
};

このうち、ListNode は、整数 val と次のノードを指すポインタ next を含むリンク リストのノードを表します。CircularLinkedList は、ヘッド ノードを指すポインタ ヘッド、テール ノードを指すポインタ テール、およびリンク リスト サイズ内のノード数を含む循環リンク リストを表します。

その他の知識ポイント

要素を削除すると次のようになります。

ここに画像の説明を挿入します

以下に示すようにノードを追加します。

次のようにシーケンス テーブルと比較します。
ここに画像の説明を挿入します

リンクリストの理論的基礎

単一リスト

単一リンク リストは、一連のノードで構成される一般的な線形データ構造であり、各ノードにはデータ要素と次のノードへのポインターが含まれます。単結合リストの最初のノードはヘッド ノードと呼ばれ、最後のノードはテール ノードと呼ばれ、テール ノードのポインタはヌル アドレスを指します。

単一リンク リストの利点は、挿入と削除の操作は時間計算量が O(1) で比較的高速であるのに対し、検索操作はリンク リスト全体を走査する必要があり、時間計算量は O(n) であることです。

単一リスト

コードは以下のように表示されます。

struct ListNode {
    
    
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {
    
    }  // 节点的构造函数(C++11的初始化列表(initializer list)语法)
};

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

  • ノードを挿入および削除するときは、リンク リスト内の隣接するノードのポインタの更新に注意してください。
  • ノードを使用した後、メモリ リークを避けるために、ノードが占有しているメモリを手動で解放する必要があります。
  • リンク リストをトラバースするときは、ポインタが null アドレスを指すまでループ構造を使用する必要があります。

二重リンクリスト

二重リンクリストの各ノードには、データ要素と、前のノードと次のノードをそれぞれ指す 2 つのポインターが含まれています。単一リンク リストと比較して、二重リンク リストには前のノードを指す追加のポインタがあり、双方向のトラバーサル操作やノードの挿入および削除操作の実行が容易になります。

C++ では、次の構造を使用して二重リンク リストのノードを定義できます。

struct ListNode {
    
    
    int val;              // 存储的元素值
    ListNode* prev;       // 指向前一个节点的指针
    ListNode* next;       // 指向后一个节点的指针
    ListNode(int x) : val(x), prev(nullptr), next(nullptr) {
    
    }  // 构造函数
};

循環リンクリスト

循環リンク リストは、最後のノードの次のノードが最初のノードを指し、リングを形成する特殊なリンク リストです。循環リンク リストは、一方向循環リンク リストと双方向循環リンク リストの 2 つのタイプに分類できます。

一方向循環リンク リストでは、各ノードには次のノードを指すポインターが 1 つだけあります。最後のノードのポインタは最初のノードを指します。

二重リンクリストでは、各ノードには 2 つのポインターがあり、1 つは前のノードを指し、もう 1 つは次のノードを指します。最初のノードの前のノードは最後のノードを指し、最後のノードの後のノードは最初のノードを指します。

循環リンク リストの利点は、最後のノードから最初のノードに戻ることができるため、循環トラバーサルを簡単に実装できることです。同時に、循環リンク リストはリンク リストの「テール ポインタ」問題を解決できます。つまり、リンク リストの最後にノードを追加するときに、ノードを見つけるためにリンク リスト全体をたどる必要がありません。最後のノード。

ただし、循環リンク リストの欠点は、実装がより複雑で、最後のノードのポインタの特別な処理が必要になることです。同時に、循環リンク リスト内のノードの数は制御が難しく、メモリ リークや無限ループなどの問題が発生しやすくなります。

一方向循環リンク リストの構造は次のとおりです。

struct ListNode {
    
    
    int val;
    struct ListNode *next;
};

struct CircularLinkedList {
    
    
    ListNode *head;
    ListNode *tail;
    int size;
};

このうち、ListNode は、整数 val と次のノードを指すポインタ next を含むリンク リストのノードを表します。CircularLinkedList は、ヘッド ノードを指すポインタ ヘッド、テール ノードを指すポインタ テール、およびリンク リスト サイズ内のノード数を含む循環リンク リストを表します。

その他の知識ポイント

要素を削除すると次のようになります。

以下に示すようにノードを追加します。

次のようにシーケンス テーブルと比較します。

画像-20200619160333275

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

このセクションは、Code Random Notes: Code Random Notes、説明ビデオ: LeetCode: 203 に対応します。リンクされたリストの要素を削除する_bilibili_bilibili

エクササイズ

質問リンク: 203. リンクされたリスト要素を削除する - LeetCode

リンク リストのヘッド ノードheadと整数を指定した場合valNode.val == val条件を満たすリンク リスト内のすべてのノードを削除し、新しいヘッド ノードを返してください。

例 1: 入力: head = [1,2,6,3,4,5,6]、val = 6 出力: [1,2,3,4,5]

例 2: 入力: head = []、val = 1 出力: []

例 3: 入力: head = [7,7,7,7]、val = 7 出力: []

私の解決策

アイデア: 一時ポインターの次のノード値が val に等しいかどうかをループして判断します。そうである場合は、一時ポインターの次のノードが一時ポインターの次のノードを指すようにします。そうでない場合は、一時ポインターが次のノードを指すようにします。ノード (トラバーサルに相当)。ただし、この方法は次のノードを判断するため、先頭ノードの状況を別途判断する必要があると同時に、if を使用すると val に等しい先頭ノードが削除される可能性があるため、if ではなく while を使用することに注意してください。次のノードは引き続き val に等しくなります。

class Solution {
    
    
   public:
    ListNode* removeElements(ListNode* head, int val) {
    
    
        // 处理特殊情况
        if (head == nullptr){
    
    
            return nullptr;  
        }
     
        // 判断首个元素等于val的情况        
        while(head->val == val){
    
    
            head = head->next;
            // 如果head已经是nullptr说明已经没元素了,直接返回nullptr
            if(head == nullptr){
    
    
                return nullptr;
            }     
        }
        ListNode* temp = head;
        // 判断的是temp的下一个元素,因此上面要加上判断首个元素的情况
        while (temp->next != nullptr) {
    
    
            // 如果下一个元素值为val,则让temp的下一个元素等于下下个元素
            if(temp->next->val==val){
    
    
                temp->next=temp->next->next;
            }else{
    
    
                // 否则temp等于下一个元素(相当于遍历单链表)
                temp = temp->next;
            }
        }
        return head;
    }
};
  • 時間計算量: O(n)。このコードは while ループを使用して単連結リストを走査します。各ループで、現在のノードの次のノードを削除する必要があるかどうかが判断されます。ループの回数は単連結リストの長さ n なので、時間は複雑さは O(n) です。
  • 空間複雑度: O(1)。このコードでは定数レベルの追加スペースのみが使用されるため、スペースの複雑さは O(1) です。

注意すべき点:

  • C++ でノードを削除した後、deleteこのノードのメモリ空間を手動で削除する必要があることに注意してください。

仮想ヘッドノード ソリューション

上記で使用した解決策は、ヘッド ノードが他のノードと矛盾しているため (ヘッド ノードの前に要素がなく、次の方法ではヘッド ノードを判定できない)、ヘッド ノードを個別に判定する必要があるためです。 。また、ヘッド ノードの前に、ヘッド ノードを指す仮想ノードを追加して、次の仮想ノードがヘッド ノードを指すようにすることで、ヘッド ノードと他のノードの処理ロジックの一貫性を保つことができます。

class Solution {
    
    
public:
    ListNode* removeElements(ListNode* head, int val) {
    
    
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead; // 临时指针
        while (cur->next != NULL) {
    
    
            if(cur->next->val == val) {
    
    
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp; // 别忘了删除内存
            } else {
    
    
                cur = cur->next;
            }
        }
        head = dummyHead->next; // 返回的是虚拟头节点的下一个节点(原始的头节点)
        delete dummyHead;
        return head;
    }
};
  • 時間計算量: O(n)。このコードは while ループを使用して単連結リストを走査します。各ループで、現在のノードの次のノードを削除する必要があるかどうかが判断されます。ループの回数は単連結リストの長さ n なので、時間は複雑さは O(n) です。
  • 空間複雑度: O(1)。このコードでは定数レベルの追加スペースのみが使用されるため、スペースの複雑さは O(1) です。

デザインリンクリスト

このセクションは Code Random Notes: Code Random Notes、解説ビデオ:リンク リストの操作を徹底的に学ぶのに役立ちます。LeetCode: 707. デザインリンクリスト_bilibili_bilibili

エクササイズ

質問リンク: 707. デザインリンクリスト - LeetCode

リンクリストの実装を設計します。単一リンク リストまたは二重リンク リストの使用を選択できます。単一リンクリスト内のノードには、val と next という 2 つの属性が必要です。val は現在のノードの値で、next は次のノードへのポインタ/参照です。二重リンク リストを使用している場合は、リンク リスト内の前のノードを示す属性 prev も必要です。リンクされたリスト内のすべてのノードが 0 インデックスであると仮定します。

これらの関数をリンク リスト クラスに実装します。

  • get(index): リンクされたリストのインデックス ノードの値を取得します。インデックスが無効な場合は、-1 が返されます。
  • addAtHead(val): リンクされたリストの最初の要素の前に値 val を持つノードを追加します。挿入後、新しいノードはリンク リストの最初のノードになります。
  • addAtTail(val): 値 val を持つノードをリンク リストの最後の要素に追加します。
  • addAtIndex(index,val): リンク リストのインデックス ノードの前に値 val を持つノードを追加します。インデックスがリンク リストの長さと等しい場合、ノードはリンク リストの末尾に追加されます。インデックスがリンク リストの長さより大きい場合、ノードは挿入されません。インデックスが 0 未満の場合、ノードを先頭に挿入します。
  • deleteAtIndex(index): インデックス インデックスが有効な場合、リンク リスト内のインデックス ノードを削除します。

例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //リンクされたリストは 1->2->3 になります
linkedList.get(1) ; //Return 2
linkedList.deleteAtIndex(1); //現在、リンク リストは 1->3 です
linkedList.get(1); //Return 3

私の解決策

質問の意味は非常に単純で、リンクされたリストに対する一般的な操作です。しかし、最初に書き始めたとき、ヘッド ノードをクラスに配置する方法がよくわかりませんでした。コードのカプリスに対する解決策をいくつか読んだ後、ノードの構造体をクラスに配置するだけで十分であることに気づきました。クラス。同じ質問の場合、ヘッド ノードの前に仮想ヘッド ノードを追加すると便利です。

ノードを挿入して次の値を割り当てるときは、順序に注意してください。たとえば、1 と 3 の間に 2 を挿入する場合、まずノード 2 の次をノード 1 の次と等しくしてから、ノード 1 の次をと等しくします。ノード2。順序が逆の場合は、まずノード 1 の次をノード 2 と等しくします。これにより、ノード 2 の次がノード 3 を見つけることができなくなります。

class MyLinkedList {
    
    
   public:
    struct Node {
    
    
        int val;
        Node* next;
        Node(int data) : val(data), next(nullptr) {
    
    }
    };
    // 初始化虚拟头节点,设置长度为0
    MyLinkedList() {
    
    
        fakeNode = new Node(0);
        size = 0;
    }
    
    int get(int index) {
    
    
        // 虽然通过了LeetCode,但这里没判断index<0的情况,要加上
        if (index + 1 > size) {
    
    
            return -1;
        }
        Node* tmp = fakeNode;
        for (int i = 0; i < index + 1; i++) {
    
    
            tmp = tmp->next;
        }
        // 遍历后tmp就是index位置的节点
        return tmp->val;
    }

    void addAtHead(int val) {
    
    
        Node* newNode = new Node(val);
        // 必须先设置newNode的next,否则就找不到真正的头节点了
        newNode->next = fakeNode->next;
        fakeNode->next = newNode;
        size++;
    }

    void addAtTail(int val) {
    
    
        Node* tmp = fakeNode;
        Node* newNode = new Node(val);
        for (int i = 0; i < size; i++) {
    
    
            tmp = tmp->next;
        }
        tmp->next = newNode;
        size++;
    }

    void addAtIndex(int index, int val) {
    
    
        if (index > size) {
    
    
            return;
        }
        if (index < 0) {
    
    
            addAtHead(val);
            return;
        }
        Node* newNode = new Node(val);
        Node* tmp = fakeNode;
        for (int i = 0; i < index; i++) {
    
    
            tmp = tmp->next;
        }
        // 遍历后tmp位于index的前一个位置
        newNode->next = tmp->next;
        tmp->next = newNode;
        size++;
    }

    void deleteAtIndex(int index) {
    
    
        // 还是没判断index<0的情况,要加上
        if (index + 1 > size) {
    
    
            return;
        }
        Node* tmp = fakeNode;
        for (int i = 0; i < index; i++) {
    
    
            tmp = tmp->next;
        }
        // 遍历后tmp位于index的前一个位置
        Node* deleteNode = tmp->next;
        tmp->next = tmp->next->next;
        delete deleteNode;  // 手动删除节点内存
        size--;
    }

    void printNode() {
    
    
        Node* tmp = fakeNode;
        for (int i = 0; i < size; i++) {
    
    
            tmp = tmp->next;
            cout << tmp->val;
        }
    }

   private:
    Node* fakeNode;
    int size;
};
  • 時間計算量の分析
    • get(int index): インデックス位置にあるノードを見つけるためにリンクされたリスト全体を走査する必要があるため、時間計算量は O(n) です。
    • addAtHead(int val): ヘッド ノードのポインティングのみを変更する必要があり、リンク リスト全体を走査する必要がないため、時間計算量は O(1) です。
    • addAtTail(int val): リンク リスト全体を走査し、末尾ノードを見つけて、その後に新しいノードを追加する必要があるため、時間計算量は O(n) です。
    • addAtIndex(intindex, int val): リンクされたリスト全体を走査し、インデックス位置で前のノードを見つけて、その後に新しいノードを追加する必要があるため、時間計算量は O(n) です。
    • deleteAtIndex(intindex): リンクされたリスト全体を走査し、インデックス位置で前のノードを見つけて、その後ろのノードを削除する必要があるため、時間計算量は O(n) です。
  • 空間の複雑さ: O(n)。このコードは、単一リンク リストのノードを表す構造体 Node と、仮想ヘッド ノードを指すポインタ fakeNode およびリンク リストの長さを記録する可変サイズを使用します。このうち、ノード数と仮想ヘッドノード数はともにn+1であるため、空間複雑度はO(n)となる。

注意点は以下のとおりです。

  • インデックスの極端なケースに注意してください。この問題ではインデックス < 0 の場合を判定しませんでした。テストケースは合格しましたが、厳密な意味で完全に正しいわけではありません。
  • 他の変数と区別できるように、コードの前や中に a を付けるなどする良いでしょsizeう。fakeNode__sizethis->size

気まぐれレコーディングコードをコード化

私のコードと同様に、私は for ループ トラバーサルを使用し、彼は while トラバーサルを使用します。コメントはすでに非常に明確なので、あまり説明しません。

class MyLinkedList {
    
    
public:
    // 定义链表节点结构体
    struct LinkedNode {
    
    
        int val;
        LinkedNode* next;
        LinkedNode(int val):val(val), next(nullptr){
    
    }
    };

    // 初始化链表
    MyLinkedList() {
    
    
        _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
        _size = 0;
    }

    // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
    int get(int index) {
    
    
        if (index > (_size - 1) || index < 0) {
    
    
            return -1;
        }
        LinkedNode* cur = _dummyHead->next;
        while(index--){
    
     // 如果--index 就会陷入死循环
            cur = cur->next;
        }
        return cur->val;
    }

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    void addAtHead(int val) {
    
    
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
    
    
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
    
    
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }

    // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果index大于链表的长度,则返回空
    // 如果index小于0,则在头部插入节点
    void addAtIndex(int index, int val) {
    
    

        if(index > _size) return;
        if(index < 0) index = 0;        
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(index--) {
    
    
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }

    // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
    void deleteAtIndex(int index) {
    
    
        if (index >= _size || index < 0) {
    
    
            return;
        }
        LinkedNode* cur = _dummyHead;
        while(index--) {
    
    
            cur = cur ->next;
        }
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        _size--;
    }

    // 打印链表
    void printLinkedList() {
    
    
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
    
    
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int _size;
    LinkedNode* _dummyHead;

};
  • 時間計算量と空間計算量は上記と同じ

逆リンクリスト

このセクションはコード ランダム レコードに対応します:コード ランダム レコード、説明ビデオ:逆リンク リストを取得するのに役立ちます | LeetCode: 206. 逆リンク リスト | ダブル ポインター メソッド | 再帰メソッド_bilibili_bilibili

エクササイズ

質問リンク: 206. 逆リンクリスト - LeetCode

単一リンク リストのヘッド ノードを指定した場合head、リンク リストを反転し、反転したリンク リストを返してください。

例:
入力: head = [1,2,3,4,5]
出力: [5,4,3,2,1]

ダブルポインタ

たとえば、上の例では、1 2 3 4 5 を反転したいとします。ポインタ cur を使用して単一リンク リストをトラバースし、別のポインタ pre を使用して cur の前のノードを指します。たとえば、cur=2 の場合、最初は 1->2 でしたが、2->1 に変更されます。これは、cur->next=pre を直接設定することで実現できますが、この場合、次の 3 つをたどることができません。したがって、一時ポインター temp を使用して、最初に cur->next を指し、次に cur->next=pre にします。

この時点で 1 と 2 の逆転は完了していますが、このままトラバースしたい場合は前後両方とも 1 つ後ろに移動する必要があります。pre、cur、temp の位置は次のようになります: pre cur temp. pre と cur を 1 つ後ろに移動するには、単に pre=cur および cur=temp とします。これら 2 つの順序を逆にすることはできないことに注意してください。逆にしないと、cur が最初に temp に等しくなります。これは、元の cur が失われるのと同じです。

class Solution {
    
    
   public:
    ListNode* reverseList(ListNode* head) {
    
    
        ListNode* cur = head;
        ListNode* pre = nullptr;
        ListNode* tmp;
        while (cur != nullptr) {
    
    
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 3 つのポインター変数のみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

再帰

まず、ダブルポインタと同様の再帰がコードカプリッチョ上に与えられます。新しい関数が定義され、初期化ステップは新しく定義された関数にパラメータを渡すように変更され、2 つのポインタを後方に移動するステップは関数を再帰的に呼び出すように変更されます。同時に、ダブル ポインターの終了条件と同様に、cur == NULL のときに再帰関数は終了します。

class Solution {
    
    
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
    
    
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
    
    
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間の複雑さ: O(n)。このアルゴリズムは再帰的な操作を使用するため、再帰の深さは最大 n レベルです。再帰の各レベルでは、現在のノードのポインタと前のノードのポインタを保存する必要があるため、空間計算量は O(n) です。

上記の再帰はダブルポインタの書き方に似ているので理解しやすいですが、LeetCodeではより簡潔な再帰が公式に提供されています。

class Solution {
    
    
public:
    ListNode* reverseList(ListNode* head) {
    
    
        if (!head || !head->next) {
    
    
            return head;
        }
        ListNode* newHead = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return newHead;
    }
};

コメント欄に詳しいコメントをくださった方がいらっしゃいました(Java版)。たとえばhead->next->next = head;、1 2 3、head=2、head->next=3、3->next=2 は 2<->3 と同等です。しかし、反転後は 2 の次は 3 を指さなくなるため、head->next = nullptr;2 の次は nullptr に設定されます。

public ListNode reverseList(ListNode head) {
    
    
        if (head == null || head.next == null) {
    
    
            /*
                直到当前节点的下一个节点为空时返回当前节点
                由于5没有下一个节点了,所以此处返回节点5
             */
            return head;
        }
        //递归传入下一个节点,目的是为了到达最后一个节点
        ListNode newHead = reverseList(head.next);
                /*
            第一轮出栈,head为5,head.next为空,返回5
            第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
                      把当前节点的子节点的子节点指向当前节点
                      此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
                      此时链表为1->2->3->4<-5
                      返回节点5
            第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
                      此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
                      此时链表为1->2->3<-4<-5
                      返回节点5
            第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
                      此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
                      此时链表为1->2<-3<-4<-5
                      返回节点5
            第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
                      此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
                      此时链表为1<-2<-3<-4<-5
                      返回节点5
            出栈完成,最终头节点5->4->3->2->1
         */
        head.next.next = head;
        head.next = null;
        return newHead;
    }

まだ少し抽象的で理解しにくいと感じる場合は、次のビデオ説明をご覧ください: Linked List Reversal (Recursive Method)_bilibili_bilibili

リンクリスト内のノードをペアごとに交換する

このセクションは、コード ランダム ノート:コード ランダム ノート、説明ビデオ:リンク リストの詳細を明確に学習するのに役立ちます。| LeetCode: 24. リンクされたリスト内のノードのペアごとの交換_bilibili_bilibili

エクササイズ

質問リンク: 24. リンクリスト内のノードのペアごとの交換 - LeetCode

リンクされたリストを与え、隣接するノードをペアで交換し、交換されたリストのヘッド ノードを返します。ノード内の値を変更せずにこの質問に回答する必要があります (つまり、ノードのスワップのみを実行できます)。


入力: head = [1,2,3,4]
出力: [2,1,4,3]

私の解決策

上記の逆リンクリストはダブルポインタを使っているので、最初に考えたのがダブルポインタを使った解決策でした。仮想ヘッド ノードを定義します。

0 1 2 3 4 の場合、cur ポインタは交換される 2 つのノードのうちの最初のノードである 1 を指します。1 の前の 0 の次も交換後に変更されるため、私の考えは 2 番目のノードを定義することですポインタ pre cur の前のノードを記録するために使用されます。

cur の次の 2 に等しい一時ポインタを定義すると、1.next=3、2.next=1、0.next=2 となります。2 が一時ポインタで保存されていない場合、次のときに 2 が見つかりません。 1.next=3 。

交換後の順序は、0 2 1 3 4 になります。このとき、pre は 0 を指し、cur は 2 を指します。Cur=cur->next により、cur は交換される次の要素の最初の位置に移動でき、その前に、pre=cur によりまず pre が交換される次の要素の前の要素に進むことができます。

while ループの終了条件は、cur が毎回最初に交換される要素を指します。偶数の場合、cur は nullptr となり、奇数の場合、cur->next は nullptr になります。

次の操作を行うと少し混乱するかもしれませんが、cur と pre はポインタであり、アドレスを記録するものであることを覚えておいてください。次の操作をどのように実行しても、アドレスの値は変わりません。つまり、最初は 0 1 2 3 4 で cur は 1 を指していましたが、交換演算後は 0 2 1 3 4 になりました。このとき、cur はまだ 1 を指していました。したがって、cur が 3 を指すようにしたい場合は、cur=cur->next と設定するだけです。

class Solution {
    
    
   public:
    ListNode* swapPairs(ListNode* head) {
    
    
        
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;

        ListNode* pre = dummyHead;//0
        ListNode* cur = head; //1
        // 0 1 2 3
        while (cur != nullptr && cur->next != nullptr) {
    
    
            ListNode* next_tmp = cur->next;//2
            cur->next = next_tmp->next;//1.next=3
            next_tmp->next = cur;//2.next=1
            pre->next = next_tmp;//0.next=2

            pre = cur;
            cur = cur->next;
        }

        return dummyHead->next;
    }
};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 3 つのポインター変数と 1 つの仮想ヘッド ノードのみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

コードランダム記録ソリューション

私のソリューションでは、ダブル ポインターと一時ポインター変数を使用しました。コード カプリッチョでは、作成者は 1 つのポインターと 2 つの一時ポインター変数を使用します。

カールの解決策では、 cur はスワップされるノードの前のノードを指し、スワップ シーケンスは次の図に示すとおりです。ステップ 1 の実行後、ノード 1 が見つからないため、一時ノードを使用してノード 1 を記録します。ステップ 2 の実行後、ノード 3 が見つからないため、一時変数を使用してノード 3 を記録します。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムが備わっている可能性があります。画像を保存して直接アップロードすることをお勧めします (img-WNQIxCg5-1679135679524)(https://code- Thinking.cdn.bcebos. com/pics/24.% E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7 %9A%84%E8% 8A%82%E7%82%B91.png)]

class Solution {
    
    
public:
    ListNode* swapPairs(ListNode* head) {
    
    
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
    
    
            ListNode* tmp = cur->next; // 记录临时节点 1
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点 3

            cur->next = cur->next->next;    // 步骤一 0.next=2
            cur->next->next = tmp;          // 步骤二 2.next=1
            cur->next->next->next = tmp1;   // 步骤三 1.next=3

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        return dummyHead->next;
    }
};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 4 つのポインター変数と 1 つの仮想ヘッド ノードのみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

しかし実際には、ここで使用できるポインターは 1 つだけで、上記の例でも 0 1 2 3 4 です。カールの解決策では、cur は 0 を指し、2 つの一時変数はそれぞれ 1 と 3 を格納するために定義されていますが、実際には 2 を格納するために必要な一時変数は 1 つだけです。

save 2 ではなぜ一時変数しか使用できないのですか? 変換は 0 2 1 3 であることがわかっています。3 つのステップの順序を見てみましょう。

1.next=3、2.next=1、0.next=2、変換された順序はたまたま後ろから前になりますが、カールの順序は前から後ろです。単一リンクリストには後方の next 順序しかないため、前から後ろに進むと、以前の next の値が変更され、 next に基づいて見つけることができるさらに多くの値が失われます1.next=3 を実行した後、元々は 1.next で 2 を見つけることができましたが、現在は 1.next=3 なので、2 を保存するには一時変数が必要です。

// cur:0
// 0 1 2 3 4
ListNode* tmp = cur->next->next;  // 记录临时节点2
cur->next->next = tmp->next;      // 步骤一 1.next=3
tmp->next = cur->next;            // 步骤二 2.next=1
cur->next = tmp;                  // 步骤三 0.next=2

リンクリストの下からN番目のノードを削除します

このセクションは、Code Random Notes: Code Random Notes、解説ビデオ:リンク リスト トラバーサルについてよく理解する!に対応しています。| LeetCode: 19. リンクリストの下からN番目のノードを削除_bilibili_bilibili

エクササイズ

質問リンク: 19. リンクされたリストの最後から N 番目のノードを削除する - LeetCode

リンク リストを指定し、nリンク リストの最後から 2 番目のノードを削除して、リンク リストの先頭ノードを返します。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-cNZhXKXu-1679135703683) (null)]

  • 入力: head = [1,2,3,4,5]、n = 2; 出力: [1,2,3,5]
  • 入力: head = [1]、n = 1; 出力: []
  • 入力: head = [1,2]、n = 1; 出力: [1]

直感的なソリューション

この質問は非常に単純で、質問の意味は非常に明確です。仮想ヘッダー ノードを使用するのが最善であることに注意してください。そうでない場合は、例 2 を個別に処理する必要があり、少し面倒になります。考えやすいのは、下から n 番目のノードを削除したいので、最初に次のリンクされたリストをたどって合計ノード数を確認し、次に削除するノードの前のノードにたどることです。ノードの合計数のサイズと n に応じて、削除を実行します。

ここで注意する必要があるのは、トラバーサルの境界条件は、削除対象のノードよりも前のノードから削除操作を開始することであるということです。削除するノードに移動するときに、削除するノードの前のノードを取得できないため、削除できません。

class Solution {
    
    
   public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
    
    
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* tmp = head;
        int size = 0;   // 记录链表长度
        // 计算链表长度
        while (tmp != nullptr) {
    
    
            tmp = tmp->next;
            size++;
        }
        tmp = dummyHead;
        // 找到待删除节点的前一个节点
        for (int i = 0; i < size - n; i++) {
    
    
            tmp = tmp->next;
        }
        ListNode* delNode = tmp->next;
        tmp->next = tmp->next->next;
        delete delNode;
        return dummyHead->next;
    }
};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を 2 回走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 3 つのポインター変数と 1 つの仮想ヘッド ノードのみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

最適化できるポイント:

  • 今日公式のソリューションを見て、ListNode* dummyHead = new ListNode(0);dummyHead->next = head;この 2 つの文が実際にListNode* dummyHead = new ListNode(0, head);この文で実現できることに突然気づきました。ListNodeのコンストラクタ内にこのような文があるので、ListNode(int x, ListNode *next) : val(x), next(next) {}ノードの次を直接指定することもできますが、2文に分けて書いた方が直感的です。

ダブルポインタ

LeetCode の質問説明の高度なセクションに、「1 回のスキャンを使用して実装してみませんか?」と記載されています。最初は考えませんでしたが、質問のプロンプトを読んだ後に理解しました: 2 つのポインターを維持し、n ステップの遅延で 1 つを更新します。

たとえば、0 1 2 3 4 5 では、ポインタ cur を使用してトラバースし、別のポインタ pre-slow cur ポインタ n+1 ステップを使用してトラバースできます。たとえば、n=2、cur=3、pre=0 (仮想ヘッド ノード) の場合、2 つのポインターは一緒に移動します。cur が 5 の後に nullptr にトラバースすると、pre は削除される前のノード 4 にちょうどトラバースします。 . ノード 3 の位置。

n ステップの遅いトラバースであれば、pre は削除するノード 4 の位置までトラバースすることになりますが、この時点ではノード 4 を削除する操作は実行できないため、n+1 ステップの遅さになります。

class Solution {
    
    
   public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
    
    
        ListNode* dummyNode = new ListNode(0);
        dummyNode->next = head;
        ListNode* cur = dummyNode;
        ListNode* pre = dummyNode;
        // cur先向前移动n+1步
        for (int i = 0; i < n + 1; i++) {
    
    
            cur = cur->next;
        }
        // 然后cur和pre一起向前移动
        while (cur != nullptr) {
    
    
            cur = cur->next;
            pre = pre->next;
        }
        ListNode* delNode = pre->next;
        pre->next = pre->next->next;
        delete delNode;
        return dummyNode->next;
    }
};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を 1 回走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 3 つのポインター変数と 1 つの仮想ヘッド ノードのみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

しかし、書いてみて少し混乱しましたが、これはまだスキャンですか? for ループと while ループが含まれているためです。よく考えてみると、それは確かにスキャンだったことがわかりました。

直感的な解決策では、最初のスキャンで、削除する必要がある前のノードを決定するために、リンクされたリストの長さを計算する必要があります。2 回目のスキャンでは、実際にノードの削除が開始されました。したがって、このソリューションでは 2 回のスキャンが必要になります。ダブルポインタ ソリューションでは、2 つのポインタを使用して、リンク リストの長さを事前に計算せずに、削除されるノードの前のノードを記録します。

極端な例として、1000 個のノードのうち、最後から 2 番目のノード、つまりノード 1000 を削除したいとします。最初の解決策では、まず 1000 個のノードを走査してリンク リストの長さが 1000 であることを確認し、次に走査する必要があります。ノード 999 に移動して、削除操作を実行します。2 番目の解決策では、最初に cur ポインタのみが 2 ステップ進む必要があり、その後、pre ポインタと cur ポインタが一緒に動き始め、最後に、cur ポインタが次のノード 1000 (nullptr) に到達すると、pre はちょうど到達します。ノード 999 の位置。この例では、解決策 1 には 1001+999=1999 ステップかかりましたが、解決策 2 には 1001+2=1003 ステップしかかかりませんでした。

なぜ 1000 ではなく 1001 なのでしょうか? 1000 から次のステップに進むにはもう 1 ステップかかるため nullptr

それでも理解できない場合は、次のコードを見てください。for ループを使用せず、サイズをカウントに使用します。実際、これは上記の解決策と同じ意味です。

class Solution {
    
    
   public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
    
    
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        ListNode* pre = dummyHead;
        int size = 0;
        while (cur != nullptr) {
    
    
            // size > n说明慢指针可以开始移动了
            if (size > n)
                // 循环结束后pre指向待删除节点的前一个节点
                pre = pre->next;
            else
                size++;
            cur = cur->next;
        }
        ListNode* delNode = pre->next;
        pre->next = pre->next->next;
        delete delNode;
        return dummyHead->next;
    }
};
  • 時間計算量: O(n)。ここで、n はリンクされたリストの長さです。アルゴリズムはリンク リスト全体を 1 回走査する必要があるため、時間の計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 3 つのポインター変数と 1 つの仮想ヘッド ノードのみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

交差するリンクリスト

このセクションはコード ランダム レコード:コード ランダム レコードに対応します。説明ビデオはまだありません。

エクササイズ

質問リンク: 160. 交差するリンクリスト - LeetCode

headA2 つの単一リンクされたリストとのヘッド ノードが与えられた場合headB、2 つの単一リンクされたリストが交差する開始ノードを見つけて返します。2 つのリンクされたリスト間に交差するノードがない場合は、 を返しますnull

この図は、2 つのリンク リストがノード c1 で交差し始めることを示しています。

質問データにより、チェーン構造全体にループがないことが保証されます。

関数が結果を返した後、リンク リストは元の構造を維持する必要があることに注意してください。

カスタマイズされた評価:
評価システムへの入力は次のとおりです (この入力は、設計したプログラムには適用されません)。

  • intersectVal - 交差の開始ノードの値。交差ノードがない場合、この値は 0 になります。
  • listA - 最初のリンクされたリスト
  • listB - 2 番目のリンクされたリスト
  • SkipA - 交差ノードにジャンプする listA 内のノードの数 (ヘッド ノードから開始)
  • SkipB - 交差ノードにジャンプする listB 内のノードの数 (ヘッド ノードから開始)
    評価システムは、これらの入力に基づいて連鎖データ構造を作成し、2 つのヘッド ノード headA と headB をプログラムに渡します。プログラムが交差するノードを正しく返した場合、解決策は正しいとみなされます。

例 1:

入力: intersectVal = 8、listA = [4,1,8,4,5]、listB = [5,6,1,8,4,5]、skipA = 2、skipB = 3
出力:交差「8」
の説明: 交差するノードの値は 8 です (2 つのリンクされたリストが交差する場合、0 にすることはできないことに注意してください)。
それぞれのリストの先頭から数えて、リンク リスト A は [4,1,8,4,5]、リンク リスト B は [5,6,1,8,4,5] になります。
A では交差ノードの前に 2 つのノードがあり、B では交差ノードの前に 3 つのノードがあります。
— リスト A と B の値 1 を持つノード (A の 2 番目のノードと B の 3 番目のノード) は異なるノードであるため、交差するノードの値は 1 ではないことに注意してください。つまり、これらはメモリ内の 2 つの異なる場所を指しますが、リスト A とリスト B の値 8 を持つノード (A の 3 番目のノードと B の 4 番目のノード) はメモリ内の同じ場所を指します。

上級: O(m + n) 時間かかり、O(1) メモリのみを使用するソリューションを設計できますか?

質問の説明

質問、特に面接の質問を見ると、多くの友人が少し混乱すると思います02.07. リンク リストの交差 - LeetCode. 2 つの質問は同じですが、160 . . リンク リストの交差 - LeetCodeのトピックの説明が明確になります。 。

上の例を見ると、交差するノードがなぜ 1 ではないのか疑問に思う人も多いでしょう。しかし、8。タイトルにノードが同じとありますが、例ではAの1とBの1は異なるアドレスですが、2つのアドレスの値は同じです。たとえば、A のノード 1 のアドレスは 0x1111、B のノード 1 のアドレスは 0x2222 です。A のノード 4 の次は、アドレス 0x1111 にある値 1 のノードを指し、B のノード 6 の次は、アドレス 0x2222 にある値 1 のノードを指します。2 つのリンク リストのノード 8 のアドレスは同じであるため、ノード 8 は 2 つのリンク リストが交差するノードです。

2つのノードが同じかどうかの判定方法は、2つのポインタが同じかどうかを判定するだけでよく、例えば をcurA==curB使ってcurA->val==curB->val判定すると、上記のプログラム例では正解の8ではなく1が出力されます。

また、ローカル コードは公式の評価システムとは異なります。つまり、上の例で 2 つのリンク リストを作成した場合、同じ値を持つノード アドレスは必ずしも同じであるとは限らず、評価システムは正しい値を見つけます。 1 つは、skipA および SkipB パラメータに基づく結果です。したがって、ローカルで実行すると正解が得られない可能性が高く、評価システムに提出する必要があります。

ブルートフォースソリューション

この解決策は、交差するノードがあるかどうかを判断する必要があるため、単純かつ粗雑です。次に、リンク リスト A の各ノードを直接走査します。ノードごとに、B の各ノードを走査して、現在のノードと同じノードがあるかどうかを確認します。同じである限り、後続のノードは同じであり、現在のノードを直接返すだけです。

curA == curB は、ノードとアドレスが同じであることを意味し、curA と curB の次のものが同じでなければなりません。

class Solution {
    
    
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    
    
        ListNode* curA = headA;
        ListNode* curB = headB;
        while(curA!=nullptr){
    
    
            curB = headB;
            while(curB!=nullptr){
    
    
                if(curA == curB){
    
    
                    return curA;
                }
                curB = curB->next;
            }
            curA = curA->next;
        }
        return nullptr;
    }
};
  • 時間計算量: O(mn)。ここで、m と n は、それぞれリンク リスト headA と headB の長さです。アルゴリズムは headA と headB のすべてのノードを走査する必要があるため、時間計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 2 つのポインター変数のみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

先進的なソリューション

図に示すように、2 つのリンク リストが交差する場合、後続のノードは同じである必要があります。次に、まず、2 つのリンク リストのノード数の差分だけ、より多くのノードを含むリンク リストを後方に移動します。次の図では、B のポインタ curB が最初に b2 の位置に移動し、curA ポインタが a1 を指し、その後 2 つのポインタが 1 つずつ後方に移動して、2 つのポインタが等しいかどうかが判断されます。それらが等しい場合、ノードが同じであることを意味し、現在のポインタが返されます。

コードは次のとおりです。一般的な考え方は、まず 2 つのリンクされたリストの長さを計算し、次に 2 つのポインターを対応するステップ数だけ後方に移動します (ノード数が少ない場合のステップ数は 0)。 、 while ループはポインタが等しいかどうかを判断し、等しくない場合は、それぞれ 1 位置後方に移動します。

class Solution {
    
    
   public:
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
    
    
        int sizeA = 0, sizeB = 0;
        ListNode* dummyHeadA = new ListNode(0,headA);
        ListNode* dummyHeadB = new ListNode(0,headB);
        ListNode* curA = dummyHeadA;
        ListNode* curB = dummyHeadB;
        // 计算两个单链表的长度,循环结束后curA和curB指向的是各自链表最后一个节点
        while (curA->next != nullptr) {
    
    
            curA = curA->next;
            sizeA++;
        }
        while (curB->next != nullptr) {
    
    
            curB = curB->next;
            sizeB++;
        }
        // 如果两个链表的最后一个节点不同,说明一定没有相交的节点
        if (curA != curB)
            return nullptr;
        curA = headA;
        curB = headB;
        // 计算两个指针移动的步数
        int moveA = sizeA > sizeB ? sizeA - sizeB : 0;
        int moveB = sizeB > sizeA ? sizeB - sizeA : 0;
        // 两个指针分别移动各自的步数
        for (int i = 0; i < moveA; i++)
            curA = curA->next;
        for (int i = 0; i < moveB; i++)
            curB = curB->next;
        // 开始向后遍历,当两个节点相同时则返回
        while (curA != nullptr) {
    
    
            if (curA == curB) {
    
    
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return nullptr;
    }
};
  • 時間計算量: O(m+n)。ここで、m と n は、それぞれリンク リスト headA と headB の長さです。アルゴリズムは headA と headB のすべてのノードを走査する必要があるため、時間計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 4 つのポインター変数と 2 つの仮想ヘッド ノードのみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

ダブルポインターソリューション

このダブルポインタ ソリューションは、LeetCode の公式ソリューションであるIntersecting Linked List - Intersecting Linked List - LeetCodeから来ています。実際には上記の高度なソリューションと同じアイデアですが、公式のソリューションは簡潔すぎます。

class Solution {
    
    
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    
    
        if (headA == nullptr || headB == nullptr) {
    
    
            return nullptr;
        }
        ListNode *pA = headA, *pB = headB;
        while (pA != pB) {
    
    
            pA = pA == nullptr ? headB : pA->next;
            pB = pB == nullptr ? headA : pB->next;
        }
        return pA;
    }
};
  • 時間計算量: O(m+n)。ここで、m と n は、それぞれリンク リスト headA と headB の長さです。アルゴリズムは headA と headB のすべてのノードを走査する必要があるため、時間計算量はリンク リストの長さに比例します。
  • 空間複雑度: O(1)。これは、アルゴリズムが一定レベルの追加スペース、つまり 2 つのポインター変数のみを使用するためです。入力リンク リストの長さに関係なく、アルゴリズムに必要な追加スペースは固定されており、リンク リストの長さとは関係ありません。

まず、リンク リストの headA またはリンク リストの headB が空である場合の特殊な場合を判断します。リンク リストのいずれかが空の場合は、交差するノードがないことを意味し、nullptr が直接返されます。

次に、リンク リスト headA とリンク リスト headB のヘッド ノードをそれぞれ指す 2 つのポインタ pA と pB が定義されます。

次に、コードはループに入り、pA と pB が同じノードを指しているかどうかを判断し、そうでない場合はループを継続します。ループ内では、pA と pB はそれぞれ 1 ノードずつ後方に移動します。リンク リストの最後に移動すると、もう一方のリンク リストのヘッド ノードを指します。これにより、2 つのポインタがリンク リストの headA とリンク リストを確実に横断するようになります。交差するノードが見つかるまで headB を同時にリストするか、すべてのノードを走査します。

最後に、コードは pA または pB を返します。これらは交差ノードを指しているためです。交差するノードがない場合は、nullptr が返されます。

ここで、pA = pA == nullptr ? headB : pA->next;次のコードのように記述できます。

if (pA == nullptr) {
    
    
    pA = headB;
} else {
    
    
    pA = pA->next;
}

なぜ自分のリンクリストをたどった後、別のリンクリストの先頭を指すポインタが同時に交差するノードに到達できるのかというと、次のような理由が考えられます。

リンクリスト headA と headB の長さはそれぞれ m と n です。連結リスト headA の素部分に a 個のノードがあり、連結リスト headB の素部分に b 個のノードがあり、2 つの連結リストの交差部分に c 個のノードがあると仮定すると、a+c=m、b+c= n.

ポインタ pA はリンク リスト headA を移動し、ポインタ pB はリンク リスト headB を移動します。2 つのポインタが同時にリンク リストの末尾ノードに到達することはありません。その後、ポインタ pA はリンク リストの先頭ノードに移動します。リンクリスト headB にポインタ pB がリンクリスト headA の先頭ノードに移動し、その後 2 つのポインタは移動を続け、ポインタ pA が a+c+b 回移動し、ポインタ pB が b+c+a 回移動した後、 2 つのポインタは、2 つのリンク リストが交差するノードに同時に到達します。このノードは、2 つのポインタが交差するノードでもあります。一度に同時にポイントされるノードであり、交差するノードが返されます。時間。

連結リストが交差しない場合、ポインタ pA が m+n 回移動し、ポインタ pB が n+m 回移動すると、両方のポインタが同時に null になり、この時点で null が返されます。つまり、両方のポインタは最大で m+n 回移動できます。

理由説明はLeetCode公式より

たとえば、リンク リスト headA のノードは 1 2 7 8、リンク リスト headB のノードは 4 5 6 7 8 です。すると、a = 2、b = 3、c = 2となります。

  • PA が headA の 8 を指すとき、PB は headB の 7 を指します。
  • 次に、PA は headB のヘッド ノード レベル 4 を指し、PB は headB のレベル 8 を指します。
  • 次に、PA は headB の 5 を指し、PB は headA の 1 を指します。
  • 次に、PA は headB の 6 を指し、PB は headA の 2 を指します。
  • 次に、PA は headB の 7 を指し、PB は headA の 7 を指します。
  • 2 つのノードが同じ場合は PA を返します。

PA は a+c+b=2+2+3=7 ステップを実行し、PB は b+c+a=3+2+2=7 ステップを実行し、残りのリンク リストにはまだ c=2 ステップが残っています。残りのステップはなぜ c ですか? どちらのポインターも最大で m+n 回、つまり a+c+b+c ステップ移動できるためです。PA と PB は異なる順序で移動しますが、どちらも前の abc を削除し、最後のステップ c だけを残します。

循環リンクリスト II

このセクションは Code Random Notes: Code Random Notes、解説ビデオ:循環リンク リストをわかりやすく解説!に対応しています。循環リンクリストをどのように判断するか? 循環リンクリストのエントリを見つけるにはどうすればよいですか? LeetCode: 142. 循環リンクリスト II_bilibili_bilibili

エクササイズ

質問リンク: 142. 循環リンクリスト II - LeetCode

リンク リストのヘッド ノード head を指定すると、リンク リストがループに入り始める最初のノードを返します。リンクされたリストにループがない場合は、null が返されます。

リンクされたリスト内に次のポインタを継続的に追跡することによって再度到達できるノードがある場合、リンクされたリスト内にサイクルが存在します。指定されたリンク リスト内のリングを表すために、評価システムは内部で整数 pos を使用して、リンク リストの末尾が接続されるリンク リスト内の位置を表します (インデックスは 0 から始まります)。pos が -1 の場合、リンクされたリストにはサイクルがありません。注: pos はパラメータとして渡されません。リンクされたリストの実際の状況を識別するためにのみ使用されます。

リンクリストの変更は許可されません。

例 1:

入力: head = [3,2,0,-4]、pos = 1
出力: インデックス 1 のリンク リスト ノードを返す
説明: リンク リストにリングがあり、その末尾が 2 番目のリングに接続されています。ノード。

上級: O(1) スペースを使用してこの問題を解決できますか?

私の解決策

最適ではない解決策、単なる個人的な解決策の記録

私のアイデアは、各ノードを走査し、now を使用して仮想ヘッド ノードから現在のノードまでの距離を記録し、last を使用して以前の now 値を記録することです。各ノードについて、開始ノードから逆方向にたどり、現在と最後を比較します。

上の図の 3 2 0 -4 のように、サイクルが発生すると、cur が -4 を指すとき、仮想ヘッド ノードから -4 までの距離は 4 になります。次のノード (2) を通過すると、仮想ヘッド ノードから 2 までの距離 距離はわずか 2 です。つまり、仮想ヘッド ノードから現在のノードまでの距離が次のノードまでの距離よりも短い場合、リングが発生したに違いなく、現在のノード -4 がノードの「右端」のノードであることを意味します。リング、次のノード 2 がリングへの入り口です。つまりnow<last、ノードの次のノードが自分自身である場合、つまり であるためnow=last、判定条件はnow<=last

class Solution {
    
    
   public:
    ListNode* detectCycle(ListNode* head) {
    
    
        ListNode* dummyHead = new ListNode(0,head);
        ListNode* cur = head;
        int now = 0,last = 0;   // now记录从虚拟头节点到当前结点的距离,last保存上一个now的值
        while(cur!=nullptr){
    
    
            ListNode* tmp = dummyHead;
            now = 0;
            while(tmp!=cur){
    
    
                tmp = tmp->next;
                now++;
            }
            // 核心语句:当last值小于等于now值说明cur是环的起点
            if(now <= last){
    
    
                return cur;
            }
            last = now; // last保存上一个now值
            cur = cur->next;
        }
        return nullptr;
    }
};
  • 時間計算量: O(n^2)。このコードの中心となるのは 2 つのネストされた while ループで、外側のループはリンク リスト内の各ノードを走査し、内側のループは仮想ヘッド ノードからリンク リスト内の現在のノードまでの距離を走査します。したがって、合計の時間計算量は O(n^2) になります。
  • 空間複雑度: O(1)。このコードは、一定レベルの追加スペースのみを使用します。つまり、いくつかのポインターと 2 つの整変数を定義しているため、スペースの複雑さは O(1) です。

ただし、この解の時間計算量は O(n^2) であり、最適解ではありません。重複が発生するかどうかを判断するためにハッシュ テーブルを使用する場合、必要なのは O(n) だけです。しかし、自分の解決策を記録しておきたかっただけなので、拡張する方法として書き留めました。コードカプリッチョのハッシュテーブルは後の章にあり、ハッシュテーブルはこの問題に対する最適な解決策ではないため、ハッシュテーブルの解決策はここには含まれていません。

ダブルポインタ

1. リングがあるかどうかを判断するにはどうすればよいですか?

高速ポインタと低速ポインタの 2 つのポインタを使用します。高速ポインタは一度に 2 ステップを実行し、低速ポインタは一度に 1 ステップを実行します。リングがある場合、リング内のどこかで会わなければなりません。

理由: リングがある場合、2 つのポインターは最終的にリング内で回転し続ける必要があり、高速ポインターは毎回低速ポインターよりも 1 ステップ多くかかるためです。速いポインタと遅いポインタの間の距離は1ステップずつ減少し、最終的に両者の距離は0、つまり出会うことになることが分かります。

2. リングがあることを確認した後、リングの入り口をどのように判断するか?

ここでは数学的な導出が必要です。以下の図に示すように、a はヘッド ノードからリング入口までの距離、b はリング入口からミーティング ポイント (紫色) までの距離、c はミーティング ポイントからリング入口までの距離です。この場合、高速ポインタの移動距離は S f = a+n(b+c)+b、低速ポインタの移動距離は S s =a+b となります。

S f =2S s つまり a+n(b+c)+b = 2(a+b) つまり a=c+(n-1)(b+c)

つまり、ヘッド ノードからリングの入り口までの距離は、n-1 個のリングの距離に c を加えたものに正確に等しいということです。つまり、高速ポインタと低速ポインタが出会うとき、別のポインタの開始点を次のように定義できます。ヘッドノード、これは毎回スローポインタと同じです、彼らが一歩踏み出せば、上の式によれば、リングの入り口で必ず出会うことになります。

class Solution {
    
    
public:
    ListNode *detectCycle(ListNode *head) {
    
    
        ListNode* fast = head;
        ListNode* slow = head;
        // 链表为空或者只有一个节点且无环时直接返回nullptr
        while(fast != nullptr && fast->next != nullptr) {
    
    
            // 慢指针走一步,快指针走两步
            slow = slow->next;
            fast = fast->next->next;
            // 快慢指针相遇,此时从head和相遇点,同时移动直至相遇
            if (slow == fast) {
    
    
                ListNode* ptr = head;
                while (ptr != slow) {
    
    
                    ptr = ptr->next;
                    slow = slow->next;
                }
                return ptr; // 返回环的入口
            }
        }
        return nullptr;
    }
};
  • 時間計算量: O(n)。ここで、n はリンク リスト内のノードの数です。最初に高速ポインタと低速ポインタが一致するかどうかを判断するとき、低速ポインタが移動する距離はリンクされたリストの全長を超えず、その後ループのエントリ ポイントを検索するときに移動する距離はリンクされたリストの全長を超えません。リスト。したがって、合計実行時間は O(n)+O(n)=O(n) となります。
  • 空間複雑度: O(1)。使用するポインターは、slow、last、ptr の 3 つだけです。

要約する

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-58l5BCKq-1679814432573)(https://code- Thinking-1253855093.file.myqcloud) .com/pics/% E9%93%BE%E8%A1%A8%E6%80%BB%E7%BB%93.png#pic_center)]

おすすめ

転載: blog.csdn.net/zss192/article/details/129779534