scala模拟链表并解答约瑟夫问题

Posted 低端码农

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了scala模拟链表并解答约瑟夫问题相关的知识,希望对你有一定的参考价值。

先简单介绍下什么是约瑟夫问题,以下文字源自百度百科。


约瑟夫问题(有时也称为约瑟夫斯置换,是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”。)

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。


约瑟夫问题可以用以下文字提出:

设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。


解决约瑟夫问题的主要思想是利用环形链表,下面介绍下使用scala语言模拟链表并通过环形链表解决约瑟夫问题。在介绍环形链表之前,首先介绍单向非环形链表和双向非环形链表。


单向链表由多个节点组成,每一个节点既维护着自己的数据,又有一个指针指向下一个节点,从而形成链状,它的特点是只能从前往后查找数据,而不能反向,如下图所示:



模拟链表有多种方法,此处我们借助一个head节点,head节点是整个链表唯一暴露在外的节点,即提供了head节点,就可以依次得到以后的节点。但是这个head节点作为辅助节点,本身是不存放数据data的,只有一个指向下一个节点的指针next。其余节点内部都维护着两个域,一个数据域,一个指针域。

因此,我们可以将节点抽象成一个class类,并提供两个变量data和next。

/**
 * 利用一个内部类定义节点
 */
class Node[T] {
//数据域
 var t: T = _
//指针域
 var next: Node[T] = _

def this(t: T) {
this
   this.t = t
}
}


Node类可以定义成链表类的内部类,链表类需要维护一个head属性,和链表长度size。


//头节点,只对外暴露头节点
var head: Node[T] = new Node[T]()
//链表的长度
var size: Int = _


创建链表对象时,即初始化其head属性,其中数据域data和指针域next皆为null


定义添加元素的方法:添加元素时,默认向链表的最后位置添加节点,需要找到末尾节点,末尾节点的next指针为null,因此我们可以以此为查找条件。我们需要定义一个临时变量tmp,判断tmp的next指针是否为空,如果不为空,则将tmp指向它的下一个节点,直到找到末尾节点,然后将末尾节点的next指向新添加的节点。代码实现如下:


/**
 * 添加元素
 *
 * @param t
 */
def add(t: T): Unit = {

val node = new Node[T](t)

//刚开始时该临时节点就是头节点
 var tmp: Node[T] = head
 while (tmp.next != null) {
tmp = tmp.next
 }
tmp.next = node
//链表长度 + 1
 size += 1
}


定义删除元素的方法:删除元素时,需要提供要删除元素的索引(目标节点),然后将目标节点的前一个节点的next指针指向其下一个节点,目标节点没有被引用,则会被当成垃圾被垃圾回收期回收。如图所示:

scala模拟链表并解答约瑟夫问题

需要说明的是,每个节点对应的索引严格来说是不存在的,我们要针对索引对链表进行操作时,都是从head节点开始,假设head节点的索引为-1,则往后移动一次则令其加一。因此删掉节点后,并不用考虑其索引的变化。

我们先定义一个根据索引返回对应节点的方法:

/**
 * 根据索引返回节点对象
 * @param i
 * @return
 */
private def getNode(i: Int): Node[T] = {
if (i < 0 || i >= size) {
throw new ArrayIndexOutOfBoundsException("索引不正确!")
}
var tmp: Node[T] = head
 var counter: Int = -1
 while (tmp.next != null) {
if (i == counter + 1) {
tmp = tmp.next
     return tmp
}
tmp = tmp.next
   counter += 1
 }
tmp
}


如果我们要删除索引为i的节点,则需要找到i-1的节点,需要注意i=0的情况。

代码如下:


/**
 * 删除节点
 *
 * @param i
 */
def delete(i: Int): Unit = {
if(i==0){
head.next=head.next.next
   return
 }
val preNode: Node[T] = getNode(i-1)
preNode.next = preNode.next.next
}


指定位置插入元素的方法,不做解析,直接上代码:

/**
 * 在指定位置插入新元素
 *
 * @param i
 * @param t
 */
def add(i: Int, t: T): Unit = {
val newNode = new Node[T](t)
//获取该位置的老节点
 val oldNode: Node[T] = getNode(i)
if (i == 0) {
//在首位插入,则将新节点的指针指向老节点即可
   newNode.next = oldNode
} else if (i == size - 1) {
//在末尾插入,将老节点的指针指向新节点
   oldNode.next = newNode
} else {
//老节点的前节点的指针指向新节点
   getNode(i - 1).next = newNode
//新节点的指针指向老节点
   newNode.next = oldNode
}
}


根据索引返回具体的值和设置(修改)值的方法:

/**
 * 根据索引获取元素
 *
 * @param i
 * @return
 */
def get(i: Int): T = {
getNode(i).t
}

/**
 * 修改指定索引的值
 *
 * @param i
 * @param v
 */
def set(i: Int, v: T): Unit = {
getNode(i).t = v
}


返回所有元素的方法:


/**
 * 获取全部元素,返回一个数组
 */
def listAll: ArrayBuffer[T] = {

//创建一个为空的可变数组
 val array = new ArrayBuffer[T]()

var tmp: Node[T] = head
 while (tmp.next != null) {
tmp = tmp.next
   array.append(tmp.t)
}
array
}


以上为单向非循环链表的主要代码,下面介绍一下双向链表。


双向链表既可以从前往后查找,也可以从后往前查找,即根据一个节点,可以得到它的前一个节点,也可以得到后一个节点。在Node类中,需要加一个属性preNode,指向前一个节点。在单向链表的基础上很好实现,此处不做分析,也不列出代码。


环形(单向)链表,就是尾节点的next指针指向头节点head。如图所示:

scala模拟链表并解答约瑟夫问题


环形链表增删该查的操作与单向非环形链表基本类似,需要注意的地方:

1、非环形单向链表判断节点是否为尾节点的条件为:tmp.next == null,而环形链表判断节点是否为尾节点的条件为tmp.next == head

2、初始化环形链表时,需要将head的next指针指向自己,如:

//头节点
val head: Node[T] = new Node[T]()
//初始化时,得到一个头节点,头节点的指针指向自己
head.next = head
//链表长度
var size: Int = _

3、添加元素时,需要将新节点的next指针指向head,如:

/**
 * 添加元素
 *
 * @param t
 */
def add(t: T): Unit = {

val node = new Node[T](t)
//在末尾添加节点,则新节点的指针应该指向head
 node.next=head
 //需要得到最后一个元素,而判断条件为tmp.next == head
 var tmp: Node[T] = head
 while (tmp.next != head) {
tmp = tmp.next
 }
tmp.next = node
size += 1
}
/**
 * 在指定位置添加元素
 * @param i
 * @param t
 */
def add(i:Int,t:T): Unit ={
//获取老节点
 val oldNode: Node[T] = getNode(i)

val newNode:Node[T] = new Node[T](t)

if(i==0){
//在首位添加元素,需要将head指向newNode,newNode指向 oldNode
   head.next = newNode
newNode.next = oldNode
}else{
getNode(i-1).next = newNode
newNode.next = oldNode
}
}


其他方法与非环形链表相同,此处不做介绍。


但是上面在单向非环形链表的基础上实现环形链表有个问题,就是head节点的数据为null,似乎打断了环形链表。下面我们用环形链表解决约瑟夫问题,并完善环形链表。


假设有五个小孩围成一个圆圈,我们需要创建一个五个节点的环形队列,为了方便,我们用数字代替小孩,如图所示:

scala模拟链表并解答约瑟夫问题


节点类与上面单向链表中的相同,只是为了方便去掉了泛型:

class Node{
var id:Int = _
var next:Node=_
def this(id:Int){
this
   this.id = id
}
}


环形链表类的两个属性与上面相同,只是first节点(即head)没有赋值:

var first:Node = _
var size:Int =_


链表类只提供一个在尾部添加元素的方法:

def add(i:Int): Unit ={
val newNode:Node = new Node(i)
if(first == null){
//添加的第一个节点
   first = newNode
first.next = first
 }else{
var tmp:Node = first
   while(tmp.next!=first){
tmp = tmp.next
   }
//得到最后一个元素
   tmp.next = newNode
newNode.next = first
 }
size +=1
}


需要注意的是,我们在添加元素时,首先判断了first节点是否为空,如果为空,则将其赋值,并指向自己,这样就解决了上述环形链表head数据为null的问题。再次添加时根据条件tmp.next==first找到尾节点,将尾节点的next指针指向新节点,新节点的指针指向first节点。


写一个for循环添加五个元素到循环链表中模拟5个小孩围城圆圈:

//创建一个环形链表
val circle = new CirCle
//往环形链表中添加元素
for(i<-1 to childNum){
circle.add(i)
}

其中childNum由外层方法传入。至此我们已经创建了一个5个元素的环形链表。


我们需要两个指针,指向第一个小孩,一个为first,一个为tail,两个指针根据传入的要开始报数的小孩位置移动,first指针指向该小孩,而tail指针指向该小孩的前一个小孩,比如从第三个小孩开始报数(startNo=3),连个指针的位置如图:


先定位这两个指针:

//定义两个指针
var first = circle.first
var tail = circle.first
//tail指针指向链表尾部
while(tail.next!=first) {
tail = tail.next
}

//first指向启动的位置,即startNo,tail跟着移动到startNo-1的位置
for(i<-1 until startNo){
first = first.next
 tail = tail.next
}


假设报数范围是0到step,即firt指针和tail指针需要移动step次,first指针定位到要出队的小孩,然后将first指针指向下一个小孩,并将tail指针指向first,则被选中的小孩出队。一直循环,直到tail指针和first指针相等,则表明只剩一个小孩,跳出循环。

while(tail!=first){
//开始数
 for(i<-1 until step){
first = first.next
   tail = tail.next
 }
println(""+first.id+"个小孩被淘汰")
first = first.next
 tail.next =first
}
//只剩一个小孩
println(""+first.id+"个小孩被淘汰")

最后剩下一个小孩,则该小孩最后一个被淘汰。


如果是5个小孩,第2个开始报数,报到3则出队,结果为:



如果是500个小孩呢?只需要修改小孩个数,即可在几毫米内得到正确的答案。

以上是关于scala模拟链表并解答约瑟夫问题的主要内容,如果未能解决你的问题,请参考以下文章

循环链表与约瑟夫链表问题

约瑟夫环问题

codves1282 约瑟夫问题 链表 会 T

java 环形链表实现约瑟夫(Joseph)问题

单向环形链表解决Josephu(约瑟夫)问题

单向环形链表解决Josephu(约瑟夫)问题