去课观-银行帮面试,问到了这个经典题目,写了一半就被叫停被赶走了。回来继续写完。
双向链表实现:
* index.php 写个简单的测试用例(case)验证基本是对的(works)。
<?php
function __autoload($className) {
include dirname(__FILE__).'/'.$className.'.php';
}
echo DblLinkedListImpl::solution(10, 3).PHP_EOL;
echo RecursiveImpl::joseph(10, 3).PHP_EOL;
* DblLinkedListImpl.php
主流程
<?php
class DblLinkedListImpl {
public static function solution($n, $m) {
$list = new \DoublyLinkedList();
for ($i = 1; $i <= $n; $i++) {
$list->push($i);
}
$p = $list->getHead();
while ($list->count() > 1) {
// echo $list.PHP_EOL;
$p = \DoublyLinkedList::forward($p, $m-1);
$q = $p->next;
$list->delete($p);
$p = $q;
}
return $p->value;
}
}
* DoublyLinkedList.php
自己实现的双向链表
<?php
class Node {
/** @var mixed */
public $value;
/** @var Node|null */
public $next;
/** @var Node|null */
public $prev;
public function __construct($data) {
$this->value = $data;
$this->next = null;
$this->prev = null;
}
public function __destruct() {
$this->next = $this->prev = null;
unset($this->value);
}
}
class DoublyLinkedList implements Countable {
/** @var Node|null */
private $head;
/** @var int */
private $length;
public function __construct() {
$this->head = null;
$this->length = 0;
}
public function getHead() {
return $this->head;
}
public function isEmpty() {
return is_null($this->head);
}
public function count() {
return $this->length;
}
public function push($value) {
if ($this->isEmpty()) {
$this->head = new Node($value);
$this->head->next = $this->head;
$this->head->prev = $this->head;
} else {
$newNode = new Node($value);
$newNode->prev = $this->head->prev;
$newNode->next = $this->head;
$this->head->prev->next = $newNode;
$this->head->prev = $newNode;
// var_dump($newNode->prev->value);
// var_dump($newNode->next->value);
}
$this->length += 1;
return $this;
}
// 删除当前节点
public function delete($node) {
if ($this->isEmpty()) {
return;
}
/** @var $toDel Node */
$toDel = null;
if ($node === $this->head) {
// 删除头结点的特殊情况
$this->head->prev->next = $this->head->next;
$this->head->next->prev = $this->head->prev;
$toDel = $this->head;
$this->head = $this->head->next;
} else {
// 改变前后节点指向
$toDel = $node;
$node->prev->next = $node->next;
$node->next->prev = $node->prev;
}
$toDel->__destruct();
$this->length -= 1;
}
public static function forward($node, $n) {
while ($n-- > 0) {
$node = $node->next;
}
return $node;
}
public function forEach(callable $callback, mixed $userdata = null) {
$cur = $this->head;
$i = 0;
do {
call_user_func($callback, $cur->value, $i, $userdata);
$cur = $cur->next;
$i += 1;
} while ($cur !== $this->head);
}
public function __toString() {
if ($this->length <= 0 || $this->isEmpty()) {
return "";
}
$sep = ",";
$p = $this->head;
// 输出第0个元素
$s = $p->value;
$p = $p->next;
// 输出剩下的元素
while ($p !== $this->head && is_object($p)) {
$s .= $sep . $p->value;
$p = $p->next;
}
return $s;
}
}
递归实现:
* RecursiveImpl.php
<?php
class RecursiveImpl {
public static function joseph($n, $m) {
if($n == 1) {
return $n;
}
return (self::joseph($n - 1, $m) + $m - 1) % $n + 1;
}
}
递归
其实这道题还可以用递归来解决,递归是思路是每次我们删除了某一个士兵之后,我们就对这些士兵重新编号,然后我们的难点就是找出删除前和删除后士兵编号的映射关系。
我们定义递归函数 f(n,m) 的返回结果是存活士兵的编号,显然当 n = 1 时,f(n, m) = 1。假如我们能够找出 f(n,m) 和 f(n-1,m) 之间的关系的话,我们就可以用递归的方式来解决了。我们假设人员数为 n, 报数到 m 的人就自杀。则刚开始的编号为
…
1
…
m - 2
m - 1
m
m + 1
m + 2
…
n
…
进行了一次删除之后,删除了编号为 m 的节点。删除之后,就只剩下 n - 1 个节点了,删除前和删除之后的编号转换关系为:
删除前 --- 删除后
… --- …
m - 2 --- n - 2
m - 1 --- n - 1
m ---- 无(因为编号被删除了)
m + 1 --- 1(因为下次就从这里报数了)
m + 2 ---- 2
… ---- …
新的环中只有 n - 1 个节点。且删除前编号为 m + 1, m + 2, m + 3 的节点成了删除后编号为 1, 2, 3 的节点。
假设 old 为删除之前的节点编号, new 为删除了一个节点之后的编号,则 old 与 new 之间的关系为 old = (new + m - 1) % n + 1。
这样,我们就得出 f(n, m) 与 f(n - 1, m)之间的关系了,而 f(1, m) = 1.所以我们可以采用递归的方式来做。代码如下:
注:有些人可能会疑惑为什么不是 old = (new + m ) % n 呢?主要是因为编号是从 1 开始的,而不是从 0 开始的。如果 new + m == n的话,会导致最后的计算结果为 old = 0。所以 old = (new + m - 1) % n + 1.
int f(int n, int m){
if(n == 1) return n;
return (f(n - 1, m) + m - 1) % n + 1;
}
我去,两行代码搞定,而且时间复杂度是 O(n),空间复杂度是O(1),牛逼!那如果你想跟别人说,我想一行代码解决约瑟夫问题呢?答是没问题的,如下:
int f(int n, int m){
return n == 1 ? n : (f(n - 1, m) + m - 1) % n + 1;
}
用数组实现的话,需要unset掉元素或者给元素设置一个特殊值-1
Spl中关于双向链表的API:
https://php.net/manual/en/class.spldoublylinkedlist.php
但是没有删除节点的方法,不能使用.