C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で連結リストを並べ替える方法:リオーダーアルゴリズムの実装


問題の概要

次のような連結リストを考えます。
l1 → l2 → l3 → l4 → … → l(n-1) → ln
これを、以下の形式になるように並べ替えます。
l1 → ln → l2 → l(n-1) → …

ここでの重要な制約は、リストノードが保持している値を変更してはならないという点です。並べ替えはノード同士のつながり(next ポインタ)の付け替えだけで行う必要があります。

例えば、リストが [1,2,3,4,5] である場合、出力は [1,5,2,4,3] となります。

解法の考え方

この問題は「中央を見つける」「後半を反転する」「2つのリストを交互につなぐ」という3つのフェーズに分けて考えると分かりやすくなります。具体的には以下の手順で進めます。

ステップ1:リストを反転する reverse メソッドを定義

先頭ノード head と前のノード prev を引数に取る reverse メソッドを作成し、以下のように動作させます。

  • head が NULL の場合は prev を返す(再帰の終了条件)
  • 一時変数 temp に head の次のノードを保存する
  • head の next を prev に向け直し、prev を head に更新する
  • reverse(temp, prev) を再帰的に呼び出して結果を返す

ステップ2:リストを並べ替える reorder 処理

  • head が NULL の場合は NULL を返す
  • slow と fast という2つのノードポインタを用意し、どちらも head で初期化する(いわゆる「ウサギとカメ」のテクニックです)
  • fast および fast の次のノードがどちらも NULL でない間、slow を1つ、fast を2つずつ進めます
  • ループ終了時、slow はリストのほぼ中央に到達しています
  • fast = reverse(slow->next) として、リストの後半部分を反転します
  • slow->next を NULL に設定して前半と後半を切り離し、slow を head に戻します
  • 作業用のリストノードポインタ temp1 と temp2 を宣言します
  • fast が NULL になるまで、以下を繰り返して前半と後半を交互に結合します
    • temp1 に slow の次のノードを、temp2 に fast の次のノードを保存する
    • slow の next を fast に、fast の next を temp1 に設定して交差させる
    • slow を temp1 へ、fast を temp2 へ進める

動作の流れ([1,2,3,4,5] の場合)

  • 中央探索後:前半 [1,2,3] / 後半 [4,5]
  • 後半を反転:[5,4]
  • 交互に結合:1 → 5 → 2 → 4 → 3

計算量について補足すると、リストの長さを N としたとき時間計算量は O(N) です。空間計算量は、反転を再帰で行っているため O(N) となり、反転を反復処理に置き換えれば O(1) に抑えられます。

C++での実装例

それでは、理解を深めるために以下の実装を見てみましょう。

#include <bits/stdc++.h>
using namespace std;
class ListNode{
    public:
    int val;
    ListNode *next;
    ListNode(int data){
        val = data;
        next = NULL;
    }
};
ListNode *make_list(vector<int> v){
    ListNode *head = new ListNode(v[0]);
    for(int i = 1; i<v.size(); i++){
        ListNode *ptr = head;
        while(ptr->next != NULL){
            ptr = ptr->next;
        }
        ptr->next = new ListNode(v[i]);
    }
    return head;
}
void print_list(ListNode *head){
    ListNode *ptr = head;
    cout << "[";
    while(ptr){
        cout << ptr->val << ", ";
        ptr = ptr->next;
    }
    cout << "]" << endl;
}
class Solution {
    public:
    ListNode* successor = NULL;
    ListNode* reverse(ListNode* head, ListNode* prev = NULL){
        if(!head)return prev;
        ListNode* temp = head->next;
        head->next = prev;
        prev = head;
        return reverse(temp, prev);
    }
    void reorderList(ListNode* head) {
        if(!head)return;
        ListNode* slow = head;
        ListNode* fast = head;
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        fast = reverse(slow->next);
        slow->next = NULL;
        slow = head;
        ListNode *temp1, *temp2;
        while(fast){
            temp1 = slow->next;
            temp2 = fast->next;
            slow->next = fast;
            fast->next = temp1;
            slow = temp1;
            fast = temp2;
        }
    }
};
main(){
    vector<int> v = {1,2,3,4,5};
    ListNode *h1 = make_list(v);
    Solution ob;
    (ob.reorderList(h1));
    print_list(h1);
}

入力

[1,2,3,4,5]

出力

[1, 5, 2, 4, 3]

このコードでは、make_list 関数でベクトルから連結リストを構築し、reorderList メソッドで並べ替えを実行した後、print_list 関数で結果を出力しています。入力 [1,2,3,4,5] が [1,5,2,4,3] と正しく並べ替えられていることが確認できます。

  1. C++で「次の大きい要素」を求める方法:スタックを使った効率的なアルゴリズム

    「次の大きい要素(Next Greater Element)」とは、配列内のある要素に対して、その後ろに最初に現れるより大きい要素のことです。具体例を見てみましょう。 arr = [4, 5, 3, 2, 1] この場合、4 の次の大きい要素は 5 です。一方、3、2、1 については、後ろにより大きい要素が存在しないため、次の大きい要素は -1 となります。 アルゴリズム 配列をランダムな数値で初期化します。 スタックを初期化します。 配列の最初の要素をスタックにプッシュします。 配列の残りの要素を先頭から順に走査します。 スタックが空であれば、現在の要素をスタックにプッシュして次へ進みま

  2. C++でランダムポインタを持つリンクリストをディープコピーする方法

    ランダムポインタを持つリンクリストとはリンクリスト(連結リスト)は代表的な線形データ構造の一つで、各ノードは「ノードが保持する値(データ)」と「次のノードのアドレスを格納するポインタ(next)」という2つの部分で構成されます。本記事では、さらに各ノードがリスト内の別のノードを指す「ランダムポインタ(random)」を持つリンクリストを扱います。このようなリストに対して、元のリストと同じデータ・同じランダムポインタ構造を持つ新しいリストを作成することを、リンクリストの「ディープコピー(Deep Copy)」と呼びます。例入力:出力:5-> 2 -> 3 -> 7 ->4