约瑟夫问题

约瑟夫问题是一个经典的数学问题,也是计算机科学中常见的数据结构和算法题目之一。它的形式是:有n个人站成一排,从第一个人开始报数,每次报到m的人出列,直到所有人都出列为止。请问,最后留下的人原来在什么位置上?

这个问题可以用多种方法解决,其中包括使用数组、单链表、双向循环链表以及数学公式法等。下面我们将分别介绍这些解题方法的思路和实现。

1. 使用数组

如果我们使用数组来模拟约瑟夫问题,可以先创建一个长度为n的数组,表示n个人的编号。然后我们可以使用循环遍历该数组,并根据报数的规则删除数组中的元素,相当于把这个数组看作一个环形队列。

具体来说,从第一个人开始报数,每报到m的人就将其从数组中删除(将其值设为-1),并把后面的元素依次向前移动一位。然后从下一个人重新开始报数,直到所有人都出列。最后留下的那个人的编号就是数组中唯一一个没有被删除的数。

时间复杂度为O(nm),其中n表示人数,m表示报数到m的人出列。

#include 
#include 
using namespace std;

vector josephus(int n, int m) {
    vector res;                    // 存储出列顺序
    vector used(n, false);        // 标记每个人是否已经出列
    int count = 0, index = -1;          // count表示当前报数的人数,index表示当前报数的人的下标
    while (res.size() < n) {            // 循环直到所有人都出列为止
        index++;                        // 按顺序遍历人数列表
        if (index >= n) index = 0;      // 处理超出范围的情况
        if (!used[index]) count++;      // 如果这个人还没有出列,则更新计数器
        if (count == m) {               // 如果这个人需要出列
            res.push_back(index + 1);   // 将他的编号存储在出列顺序列表中
            used[index] = true;         // 标记这个人已经出列
            count = 0;                  // 重置计数器
        }
    }
    return res;
}

int main() {
    int n, m;
    cin >> n >> m;
    auto res = josephus(n, m);
    for (auto x : res) {
        cout << x << " ";
    }
    cout << endl;
    return 0;
}

2. 使用单链表

如果我们使用单链表来模拟约瑟夫问题,我们可以创建一个带有n个节点的单链表,并将它闭合成环形链表。然后,从头结点开始依次遍历链表,每次找到要出列的节点的前一个节点,输出并删除该节点。因为单链表只能从前往后遍历,所以每次找到要出列的节点的前一个节点需要顺时针遍历m-1个节点。

时间复杂度为O(nm),与数组解法相同。

#include 
using namespace std;

struct Node {
    int data;
    Node* next;
    Node(int d): data(d), next(nullptr) {}
};

void josephus(int n, int m) {
    Node* head = new Node(1);
    Node* prev = head;
    for (int i = 2; i <= n; i++) {
        Node* curr = new Node(i);
        prev->next = curr;
        prev = curr;
    }
    prev->next = head;  // 将链表闭合成环
    
    Node* curr = head;
    while (n > 0) {
        // 找到要出列的节点的前一个节点
        for (int i = 0; i < m-1; i++) {
            curr = curr->next;
        }
        // 输出并删除节点
        cout << curr->next->data << " ";
        Node* temp = curr->next;
        curr->next = curr->next->next;
        delete temp;
        n--;
    }

    // 释放内存
    Node* temp = head;
    while (temp != nullptr) {
        Node* next = temp->next;
        delete temp;
        temp = next;
    }
}

int main() {
    josephus(5, 3);  // 输出: 3 1 5 2 4
    return 0;
}

3. 使用双向循环链表

如果我们使用双向循环链表来模拟约瑟夫问题,我们可以创建一个带有n个节点的双向循环链表,并将它闭合成环形链表。然后,从头结点开始依次遍历链表,每次找到要出列的节点的前一个节点,输出并删除该节点。因为双向循环链表可以从前往后遍历,也可以从后往前遍历,所以每次找到要出列的节点的前一个节点需要顺时针或逆时针遍历m-1个节点。

时间复杂度为O(nm),与单链表解法相同。

#include 
using namespace std;

struct Node {
    int data;
    Node* prev;
    Node* next;
    Node(int d): data(d), prev(nullptr), next(nullptr) {}
};

void josephus(int n, int m) {
    Node* head = new Node(1);
    Node* prev = head;
    for (int i = 2; i <= n; i++) {
        Node* curr = new Node(i);
        prev->next = curr;
        curr->prev = prev;
        prev = curr;
    }
    prev->next = head;  // 将链表闭合成环
    head->prev = prev;
    
    Node* curr = head;
    while (n > 0) {
        // 找到要出列的节点的前一个节点
        for (int i = 0; i < m-1; i++) {
            curr = curr->next;
        }
        // 输出并删除节点
        cout << curr->next->data << " ";
        Node* temp = curr->next;
        curr->next = temp->next;
        temp->next->prev = curr;
        delete temp;
        n--;
    }

    // 释放内存
    Node* temp = head;
    do {
        Node* next = temp->next;
        delete temp;
        temp = next;
    } while (temp != head);
}

int main() {
    josephus(5, 3);  // 输出: 3 1 5 2 4
    return 0;
}

4. 数学公式法

除了用数据结构来模拟约瑟夫问题,还有一种数学公式法可以解决这个问题。根据数学公式,最后留下来的那个人的编号是一个关于n和m的递推公式,我们可以直接用递推公式来计算出最后留下来的人的编号。

具体来说,最后留下来的那个人的编号可以表示成:

f(n, m) = (f(n-1, m) + m) % n

其中,f(n, m)表示n个人中最后留下来的人的编号,%表示取余操作。

我们可以使用递归或循环来计算递推公式。时间复杂度为O(n),与其他算法相比更加高效。

总结

约瑟夫问题是一个经典的数学问题,也是计算机科学中常见的数据结构和算法题目之一。

我们可以使用数组、单链表、双向循环链表以及数学公式法等多种方法

你可能感兴趣的:(面试题,链表,数据结构,约瑟夫问题,算法)