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指针指向其下一个节点,目标节点没有被引用,则会被当成垃圾被垃圾回收期回收。如图所示:
需要说明的是,每个节点对应的索引严格来说是不存在的,我们要针对索引对链表进行操作时,都是从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。如图所示:
环形链表增删该查的操作与单向非环形链表基本类似,需要注意的地方:
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,似乎打断了环形链表。下面我们用环形链表解决约瑟夫问题,并完善环形链表。
假设有五个小孩围成一个圆圈,我们需要创建一个五个节点的环形队列,为了方便,我们用数字代替小孩,如图所示:
节点类与上面单向链表中的相同,只是为了方便去掉了泛型:
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模拟链表并解答约瑟夫问题的主要内容,如果未能解决你的问题,请参考以下文章