最近遇到一个算法题, 题目描述为
已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列,求出剩下的人的编号。
按照直观的思路, 这应该是一个循环链表问题。思路大概如下
- 初始化链表
- 插入节点
- 遍历链表(步长为m)
- 遇到编号为m的节点则删除
定义节点
class Node {
public $elem; // 节点数据
public $next; // 下一个节点指针
public function __construct($elem) {
$this->elem = $elem;
$this->next = null;
}
}
循环链表实现
class CircularList {
public $currentNode;
public $head;
public function __construct(Node $node) {
$this->head = $node;
$this->head->next = $this->head;
$this->currentNode = $this->head;
}
/**
在节点之后插入某节点
*/
public function insert($elem, $item) {
$curr = $this->find($item);
$node = new Node($elem);
$node->next = $curr->next;
$curr->next = $node;
}
/**
找到某节点
*/
public function find($elem) {
$currentNode = $this->head;
while ($currentNode->elem != $elem) {
$currentNode = $currentNode->next;
}
return $currentNode;
}
/**
查找当前节点的前一节点
*/
public function findPrev ($elem) {
$currentNode = $this->head;
while ($currentNode->next->elem != $elem) {
$currentNode = $currentNode->next;
}
return $currentNode;
}
public function delete ($elem) {
$node = $this->findPrev($elem);
if (isset($node->next->next)) {
$node->next = $node->next->next;
}
}
/**
前进n步
*/
public function advance($n) {
while ($n > 0) {
if ($this->currentNode->next->elem == 'head') {
$this->currentNode = $this->currentNode->next->next;
}else{
$this->currentNode = $this->currentNode->next;
}
$n--;
}
}
/*
* 列表长度
*/
public function count() {
$currentNode = $this->head;
$count = 0;
while (isset($currentNode->next) && $currentNode->next->elem != 'head') {
$currentNode = $currentNode->next;
$count++;
}
return $count;
}
// 遍历打印链表
public function display() {
$curr = $this->head;
while (isset($curr->next) && $curr->next->elem != 'head') {
echo $curr->next->elem . " ";
$curr = $curr->next;
}
}
}
有上面的代码实现, 解决这个问题就简单了,代码如下
$list = new CircularList(new Node('head'));
for ($i = 1; $i <= $n ; $i++) {
$list->insert($i, $i == 1 ? 'head' : $i - 1);
}
$total = $list->count();
while ($total > $m -1 ) {
$list->advance($m);
$list->delete($list->currentNode->elem);
$total--;
$list->display();
echo "<br/><br/>";
}
上面的代码过程都体现出来了, 但复杂度为O(mn).如果只需要得到最后的编号, 不需要这么复杂, 第一次报数, m出列, 则当前顺序为m+1, m+2, ...n, n-1, n-2, ...,m-1, 设为f'(n-1, m).对此重新编号则为1,2,...,n-1, 设为f(n-1, m).
则原问题f(n,m)转化为f'(n-1, m)的解, 我们只需要得出f'(n-1, m)与f(n-1, m)的关系即可。
第一个出列的人编号为m%n, 则他前面的人(m-1)%n, 后面的人为(m+1)%n, f与f'的对应关系为f'(n-1, m) = (f(n-1, m) + m)%n, 得到递推关系, 则此问题就简单了。
function Joseph($n,$m) {
$r=0;
for($i=2; $i<=$n; $i++) {
$r = ($r + $m) % $i;
}
return $r+1;
}