データ構造: 連結リストの基本 OJ 演習 + 双方向循環連結リストの先行実装

目次

1. リートコードの剣は、オファー II 027 を指します。回文リンク リスト

1. 問題の説明

2. 問題の分析と解決

(1) 高速および低速ポインター メソッドは、リンクされたリストの中間ノードを見つけます

(2)連結リストの後半を逆にする

添付:再帰逆リンクリスト

(3) 連結リストが回文かどうかを判定するダブルポインタ法

2.双方向循環リンクリストの実現をリードする

1.ヘッダーファイル

2.ノードメモリアプリケーションインターフェースとリンクリスト初期化インターフェース

3. リンクされたリストの印刷と検索インターフェイス

4. リンクリストの追加・削除インターフェース

5. リンクリスト破棄インターフェース


1. リートコードの剣は、オファー II 027 を指します。回文リンク リスト

ソード フィンガー オファー II 027. パリンドローム リンク リスト - Leetcode

1. 問題の説明

連結リストの先頭ノードを与えて、head,それが回文連結リストかどうかを判断してください。(回文リストなら真、回文リストでなければ偽を返す)

連結リストが回文の場合、連結リスト ノードの順序は前から後ろと後ろから前同じです。

ソリューション インターフェイス:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution 
{
public:
    bool isPalindrome(ListNode* head) 
    {

    }
};

2. 問題の分析と解決

問題を解く時間計算量が O(N) で、空間計算量が O(1)である場合、この問題の解は 3 つの部分に分割する必要があります。

  1. リンクされたリストの中間位置ノードを見つけるには、高速および低速ポインター メソッドを使用します。
  2. 連結リストの後半を逆にする
  3. ダブルポインター法を使用して、連結リストの前半と後半を比較し、連結リストが回文かどうかを判断します

(1) 高速および低速ポインター メソッドは、リンクされたリストの中間ノードを見つけます

  • 考え方としては、2 つのポインターがリンクされたリストを同時にトラバースし、高速ポインターは一度に 2 ステップ (高速 = 高速 -> 次 -> 次) を実行し、低速ポインターは一度に 1 ステップ (低速 = 低速) を実行します。 ->次へ)。

  • 高速ポインターがトラバースを終了すると、低速ポインターはちょうど中間位置のノードを指します(ノード数が奇数の連結リストの場合、低速ポインターは最後の中間ノードを指し連結リストの場合)。ノードの数が偶数の場合、スロー ポインタは最後の中間の 2 つのノードを指します。ノードの 2 番目のノード)

中間位置ノードのインターフェースを見つけます。

    ListNode * FindMid(ListNode * head)
    {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast && fast->next)   //注意循环的限制条件
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
  • 例としてノード数が偶数のリンク リストの場合を取り上げ、スロー ポインターが最終的に中央の 2 つのノードの 2 番目のノードを指すことを簡単に証明しましょう。

奇数の連結リストの場合も同様の証明ができます 。

(2) 連結リストの後半を逆にする

  • リンクされたリストの途中でノードを見つけた後、リンクされたリストの後半を逆にすることができます。

  • 連結リストの反転を完了する最良の方法は、3 点反転法 (アニメーション) です。

三点逆リンクリスト

インターフェース:

    ListNode * reverse (ListNode * head)
    {
        ListNode * cur = (nullptr == head)? nullptr : head->next;
        ListNode * pre = head;
        while(cur)
        {
            ListNode* Next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = Next;
        }
        if(head)
        head->next = nullptr;   //记得将被反转部分链表的尾结点的指针域置空
        return pre;             //pre最终指向反转链表的表头
    }

添付:再帰逆リンクリスト

再帰アルゴリズムは、単一連結リストの問題の解決策によく登場します. その理由は、再帰アルゴリズムが複数の関数スタック フレームを使用して、各連結リスト ノードのアドレスを格納できるからです(単一連結リストの欠点は、アドレス指定が難しいことです)。であるため、再帰アルゴリズムは、単一リンク リスト問題の実行可能な解決策の 1 つとしてよく使用されます(ただし、再帰アルゴリズムは、スタックをプッシュするコストが大きいため、最適な解決策ではないことがよくあります。たとえば、時間とリンクリストを逆にする再帰的な方法のスペースオーバーヘッドは、3 ポインターの反転方法よりも大きくなります。

しかし、再帰の思考訓練と理解を深める目的で、ここでは片方向リストを再帰的に反転させるアルゴリズムを分解してみます。

単一リンク リスト再帰関数の確立を逆にします。

  • リンクされたリストノードを再帰的にトラバースするためのフレームワーク:
        ListNode* reverseList (ListNode * head)
        {
            if(head->next == nullptr)//递归的结束条件
            {
                return head;
            }
            reverseList(head->next);
            return head;   
        }

    再帰フレームワークは、単一のリンクされたリストの逆トラバーサルを実装できます(図)

  • リンクされたリストのノードを逆方向にたどる再帰関数のプロセスで、ノード ポインター フィールドを変更する操作を追加できます。 
    ListNode* reverseList (ListNode * head)
    {
        if(head->next == nullptr)//递归的结束条件
        {
            return head;
        }
        reverseList(head->next);
        head->next->next = head; 
        head->next = nullptr;
        return head;
    }

ノード ポインタ ドメイン プロセス アニメーション分析を変更する再帰関数:

  • この関数が逆リンク リスト新しいヘッド ノードのアドレスを最終的な戻り値として返すことができることを願っています。
        ListNode* reverseList (ListNode * head)
        {
            if(head->next == nullptr)//递归的结束条件
            {
                return head;
            }
            ListNode* newhead = reverseList(head->next); //利用newhead将新的头节点地址逐层带回
            head->next->next = head; 
            head->next = nullptr;
            return newhead;
        }

    新しいヘッド ノード アドレスを層ごとに戻す再帰関数の図:

  • 単一リンクリストを再帰的に逆にするためのインターフェース:

        ListNode* reverseList(ListNode* head) 
        {
            if(nullptr == head || nullptr == head->next)
            //设置递归的限制条件,构建递归框架
            {
                return head;
            }
            ListNode * newhead = reverseList(head->next);
            //newhead是为了将新的头节点地址逐层带回到最外层递归函数作为返回值
            head->next->next = head;
            //从原尾结点开始实现反向链接
            head->next = nullptr;
            //这里逐层置空是为了最后将新的尾结点指针域置空
            return newhead;           
        }

再帰アルゴリズムはオーバーヘッドが比較的大きいため、 3 ポインター反転法を使用して、ソリューション インターフェイスで連結リストの反転を完了します。 

(3) 連結リストが回文かどうかを判定するダブルポインタ法

最初の 2 つの手順の後、連結リストの後半が逆になります。

最後に、二重ポインターを使用して、

ソリューション コード: 

class Solution 
{
public:
    ListNode * FindMid(ListNode * head)   //快慢指针法寻找链表中间位置节点的接口
    {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast && fast->next)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;                       //返回链表中间位置节点
    }
    ListNode * reverse (ListNode * head)   //反转链表的接口(三指针翻转法)
    {
        ListNode * cur = (nullptr == head)? nullptr : head->next;
        ListNode * pre = head;
        while(cur)
        {
            ListNode* Next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = Next;
        }
        if(head)       
        head->next = nullptr;   //记得将被反转部分链表的尾结点的指针域置空
        return pre;             //pre最终指向反转链表的表头
    }
    bool isPalindrome(ListNode* head) 
    {
        ListNode* mid = FindMid(head);
        ListNode * reversehead = reverse(mid);
        ListNode * tem = reversehead;
        while(reversehead)
        {
            if(reversehead->val != head->val)
            {
                return false;
            }
            reversehead = reversehead->next;
            head = head->next;
        }
        reverse(tem);             //恢复原链表
        return true;
    }
};

2.双方向循環リンクリストの実現をリードする

連結リストには 8 種類ありますが、実際にはほとんどの場合、使用できる連結リストは 2 種類だけです。

  1. ヘッドレス一方向非巡回連結リスト: 実際には、ヘッドレス一方向非巡回連結リストは、ハッシュ バケット、グラフの隣接リストなど、他のデータ構造の
    サブ構造
    としてよく使用されます。

  2. 先頭の双方向循環リンク リスト: この種のリンク リスト構造は、C++STL のマスターによって設計されました. 優れた構造を持ち、使用と実装がより便利です (各ノードには 2 つのポインター フィールドがあり、より多くのスペースを消費します)。 )

ヘッダー付き双方向リンク リストには、有効なデータを格納しないセンチネル ヘッド ノードがあります。

head を持つ双方向循環リンクリストの循環概略図:

1.ヘッダーファイル

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int LTDataType;

typedef struct LTNode
{
    LTDataType val;
    struct LTNode* pre;           //指向前一个节点的指针
    struct LTNode* next;          //指向下一个节点的指针
}LTNode;


//各个链表操作接口的声明
LTNode* BuyLTNode(LTDataType x);
void ListPrint(LTNode* phead);
LTNode* ListInit();
LTNode* ListFind(LTNode* phead, LTDataType x);

void ListInsert(LTNode* pos, LTDataType x);
void ListErase(LTNode* pos, LTNode* phead);
void ListPushFront(LTNode* phead, LTDataType x);
void ListPopFront(LTNode* phead);
void ListPopBack(LTNode* phead);
void ListPushBack(LTNode* phead, LTDataType x);
void ListDestory(LTNode* phead);

2.ノードメモリアプリケーションインターフェースとリンクリスト初期化インターフェース

ノード メモリ アプリケーション インターフェイス:

LTNode* BuyLTNode(LTDataType x)  //向系统申请链表节点空间的接口
{
    LTNode* NewNode = (LTNode*)malloc(sizeof(LTNode));
    if (NULL == NewNode)
    {
        perror("malloc failed:");
        exit(-1);
    }
    NewNode->next = NULL;
    NewNode->pre = NULL;
    NewNode->val = x;
    return NewNode;
}

リンクされたリストの初期化インターフェイス:

LTNode* ListInit()    //链表初始化接口(链表初始化时创建哨兵节点,接口返回哨兵节点的地址)
{
    LTNode* phead = BuyLTNode(-1);
    phead->next = phead;
    phead->pre = phead;
    return phead;
}

先頭の双方向循環リンク リストの初期化は、センチネル ノードに適用されます

使用する場合は、main 関数で LTNode 型のポインターを使用して、番兵ノードのアドレスを受け取ります

int main ()
{
    phead = ListInit();

    // 其他链表操作

    return 0;
}

3. リンクされたリストの印刷と検索インターフェイス

先頭の双方向循環リンク リストのトラバーサル プロセスは、センチネル ノードの次のノードから始まり、センチネル ノードの前のノードで終了します。

void ListPrint(LTNode* phead)     //打印链表接口(注意不要打印哨兵节点中的无效数据)
{
    assert(phead);
    LTNode* tem = phead->next;
    while (tem != phead)
    {
        printf("%d ", tem->val);
        tem = tem->next;
    }
    printf("\n");
}
LTNode* ListFind(LTNode* phead, LTDataType x)  //根据节点中存储的数据查找某个链表节点
{
    assert(phead);
    LTNode* tem = phead->next;
    while (tem != phead)
    {
        if (x == tem->val)
        {
            return tem;
        }
        tem = tem->next;
    }
    return NULL;
}

4. リンクリストの追加・削除インターフェース

  • pos アドレスのノードのインターフェースを削除します 
void ListErase(LTNode* pos, LTNode* phead)    //删除pos位置的节点
{
    assert(pos && pos != phead);
    LTNode* Pre = pos->pre;
    LTNode* Next = pos->next;
    Pre->next = Next;
    Next->pre = Pre;
    free(pos);
    pos = NULL;
}
  • pos アドレス nodeの後の位置にノードのインターフェースを挿入します。

void ListInsert(LTNode* pos, LTDataType x)    //在pos位置后插入一个链表节点的接口
{
    assert(pos);
    LTNode* newnode = BuyLTNode(x);
    LTNode* Next = pos->next;

    pos->next = newnode;
    newnode->pre = pos;
    newnode->next = Next;
    Next->pre = newnode;
}

ヘッド挿入、ヘッド削除テール挿入、テール削除のインターフェイスは、上記の 2 つのインターフェイスを多重化することで実現できます。 

void ListPushFront(LTNode* phead, LTDataType x) //头插一个节点
{
    assert(phead);
    ListInsert(phead, x);
}
void ListPopFront(LTNode* phead)                //头删一个节点
{
    assert(phead && phead->next != phead);
    ListErase(phead->next, phead);
}
void ListPopBack(LTNode* phead)                 //尾删一个节点
{
    assert(phead && phead->pre != phead);
    ListErase(phead->pre, phead);
}
void ListPushBack(LTNode* phead, LTDataType x)  //尾插一个节点
{
    assert(phead);
    ListInsert(phead->pre, x);
}
  • データの削除操作では、センチネル ノードを削除できないことに注意してください。

5. リンクリスト破棄インターフェース

void ListDestory(LTNode* phead)                 //销毁链表的接口
{
    assert(phead);
    LTNode* tem = phead->next;
    while (tem != phead)
    {
        LTNode* Next = tem->next;
        free(tem);
        tem = Next;
    }
    free(phead);
    phead = NULL;
}
  •  歩哨ノードは最後に破壊されることに注意してください

先頭の双方向循環リンク リスト各操作インターフェイスの時間計算量はO(1) であることがわかります。これは、そのデータ構造の優位性を完全に反映しています。 

 

おすすめ

転載: blog.csdn.net/weixin_73470348/article/details/129045115