解决这个问题,我的想法是递归的,在每次递归过程中,反向打印链表的下半部分,然后反向打印链表的上半部分。因此额外的空间是O(log n)
- 这是用于递归堆栈的额外空间,但它超过O(n)的时间(O(n log n) - 每个(log n)级别的组合调用递归迭代整个列表将每个部分切成两半)。
是否存在实现相同目标的算法 - 反向打印具有少于O(n)空间且最多为O(n)时间的不可变单链表?
源代码(Python 2.7):
class LinkedListNode:
def __init__(self, value, next_node):
self.value = value
self.next_node = next_node
@staticmethod
def reverse_print(list_head, list_tail):
if not list_head:
return
if not list_head.next_node:
print list_head.value
return
if list_head == list_tail:
print list_head.value
return
p0 = list_head
p1 = list_head
while p1.next_node != list_tail and p1.next_node.next_node != list_tail:
p1 = p1.next_node
p1 = p1.next_node
p0 = p0.next_node
LinkedListNode.reverse_print(p0.next_node, list_tail)
LinkedListNode.reverse_print(list_head, p0)
if __name__ == "__main__":
list_head = LinkedListNode(4, LinkedListNode(5, LinkedListNode(12, LinkedListNode(1, LinkedListNode(3, None)))))
LinkedListNode.reverse_print(list_head, None)
答案 0 :(得分:6)
这是O(n)时间和O(sqrt(n))空间算法。 在帖子的第二部分,它将扩展为线性时间和O(n ^(1 / t))空间算法,用于任意正整数t。
高级想法:将列表拆分为sqrt(n)许多(几乎)相等大小的部分。 使用从最后到第一个的朴素线性时间线性空间方法,以相反的顺序依次打印零件。
要存储部件的起始节点,我们需要一个大小为O(sqrt(n))的数组。 要恢复大小约为sqrt(n)的部分,朴素算法需要一个数组来存储对部件节点的引用。所以数组的大小为O(sqrt(n)。
一个使用大小为lsa
的两个数组(ssa
和k=[sqrt(n)]+1 =O(sqrt(n))
)
(lsa ...大步长数组,ssa ...小步长数组)
阶段1:(如果链接列表的大小未知,则找出n,其长度): 从头到尾遍历列表并计算列表的元素,这需要n步
阶段2: 将单个链表的每个第k个节点存储在数组
lsa
中。这需要n步。阶段3: 以相反的顺序处理
lsa
列表。以相反的顺序打印每个部件 这也需要n步
因此算法的运行时间为3n = O(n),其速度约为2 * sqrt(n)= O(sqrt(n))。
这是一个Python 3.5实现:
import cProfile
import math
class LinkedListNode:
def __init__(self, value, next_node):
self.value = value
self._next_node = next_node
def next_node(self):
return(self._next_node)
def reverse_print(self):
# Phase 1
n=0
node=self
while node:
n+=1
node=node.next_node()
k=int(n**.5)+1
# Phase 2
i=0
node=self
lsa=[node]
while node:
i+=1
if i==k:
lsa.append(node)
i=0
last_node=node
node=node.next_node()
if i>0:
lsa.append(last_node)
# Phase 3
start_node=lsa.pop()
print(start_node.value)
while lsa:
last_printed_node=start_node
start_node=lsa.pop()
node=start_node
ssa=[]
while node!=last_printed_node:
ssa.append(node)
node=node.next_node()
ssa.reverse()
for node in ssa:
print(node.value)
@classmethod
def create_testlist(nodeclass, n):
''' creates a list of n elements with values 1,...,n'''
first_node=nodeclass(n,None)
for i in range(n-1,0,-1):
second_node=first_node
first_node=nodeclass(i,second_node)
return(first_node)
if __name__ == "__main__":
n=1000
cProfile.run('LinkedListNode.create_testlist(n).reverse_print()')
print('estimated number of calls of next_node',3*n)
它打印以下输出(最后是分析器的输出,显示函数调用的数量):
>>>
RESTART: print_reversed_list3.py
1000
999
998
...
4
3
2
1
101996 function calls in 2.939 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 2.939 2.939 <string>:1(<module>)
2000 0.018 0.000 2.929 0.001 PyShell.py:1335(write)
1 0.003 0.003 2.938 2.938 print_reversed_list3.py:12(reverse_print)
1 0.000 0.000 0.001 0.001 print_reversed_list3.py:49(create_testlist)
1000 0.000 0.000 0.000 0.000 print_reversed_list3.py:5(__init__)
2999 0.000 0.000 0.000 0.000 print_reversed_list3.py:9(next_node)
...
estimated number of calls of next_node 3000
>>>
对next_node()的调用次数是预期的3000
不是使用朴素的O(m)空间算法以相反的顺序打印长度为m的子列表,而是可以使用该O(sqrt(m))空间算法。但我们必须在子列表的数量和子列表的长度之间找到适当的平衡:
阶段2: 将简单链表拆分为长度为n ^(2/3)的n ^(1/3)子列表。这些子列表的起始节点存储在长度为n ^(1/3)
的数组中阶段3:使用O(sqrt(m))空间算法以相反的顺序打印长度m = n ^(2/3)的每个子列表。因为m = n ^(2/3),我们需要m ^(1/2)= n ^(1/3)空间。
我们现在有一个需要4n时间的O(n ^(1/3))空间算法,所以仍然是O(n)
我们可以通过分裂成长度为m = n ^(3/4)的n ^(1/4)子列表再次重复这一点,并由O(m ^(1/3))= O(n ^ (1/4))需要5n = O(n)时间的空间算法。
我们可以一次又一次地重复这一点并得出以下声明:
大小为n的不可变简单链表可以使用t * n ^(1 / t)= O(n ^(1 / t))空间和(t + 1)n =以相反的顺序打印O(n)时间,其中t是任意正整数
如果一个不固定t但是选择t取决于n,使得n ^(1 / t))约2,最小的有用数组大小,那么这导致O(nlog(n))时间和O( log(n))空间算法由OP描述。
如果选择t = 1,则会导致O(n)时间和O(n)空间天真算法。
这是算法的实现
import cProfile
import math
import time
class LinkedListNode:
'''
single linked list node
a node has a value and a successor node
'''
stat_counter=0
stat_curr_space=0
stat_max_space=0
stat_max_array_length=0
stat_algorithm=0
stat_array_length=0
stat_list_length=0
stat_start_time=0
do_print=True
def __init__(self, value, next_node):
self.value = value
self._next_node = next_node
def next_node(self):
self.stat_called_next_node()
return(self._next_node)
def print(self):
if type(self).do_print:
print(self.value)
def print_tail(self):
node=self
while node:
node.print()
node=node.next_node()
def tail_info(self):
list_length=0
node=self
while node:
list_length+=1
last_node=node
node=node.next_node()
return((last_node,list_length))
def retrieve_every_n_th_node(self,step_size,list_length):
''' for a list a of size list_length retrieve a pair there the first component
is an array with the nodes
[a[0],a[k],a[2*k],...,a[r*k],a[list_length-1]]]
and the second component is list_length-1-r*k
and
'''
node=self
arr=[]
s=step_size
index=0
while index<list_length:
if s==step_size:
arr.append(node)
s=1
else:
s+=1
last_node=node
node=node.next_node()
index+=1
if s!=1:
last_s=s-1
arr.append(last_node)
else:
last_s=step_size
return(arr,last_s)
def reverse_print(self,algorithm=0):
(last_node,list_length)=self.tail_info()
assert(type(algorithm)==int)
if algorithm==1:
array_length=list_length
elif algorithm==0:
array_length=2
elif algorithm>1:
array_length=math.ceil(list_length**(1/algorithm))
if array_length<2:
array_length=2
else:
assert(False)
assert(array_length>=2)
last_node.print()
self.stat_init(list_length=list_length,algorithm=algorithm,array_length=array_length)
self._reverse_print(list_length,array_length)
assert(LinkedListNode.stat_curr_space==0)
self.print_statistic()
def _reverse_print(self,list_length,array_length):
'''
this is the core procedure of the algorithm
if the list fits into the array
store it in te array an print the array in reverse order
else
split the list in 'array_length' sublists and store
the startnodes of the sublists in he array
_reverse_print array in reverse order
'''
if list_length==3 and array_length==2: # to avoid infinite loop
array_length=3
step_size=math.ceil(list_length/array_length)
if step_size>1: # list_length>array_length:
(supporting_nodes,last_step_size)=self.retrieve_every_n_th_node(step_size,list_length)
self.stat_created_array(supporting_nodes)
supporting_nodes.reverse()
supporting_nodes[1]._reverse_print(last_step_size+1,array_length)
for node in supporting_nodes[2:]:
node._reverse_print(step_size+1,array_length)
self.stat_removed_array(supporting_nodes)
else:
assert(step_size>0)
(adjacent_nodes,last_step_size)=self.retrieve_every_n_th_node(1,list_length)
self.stat_created_array(adjacent_nodes)
adjacent_nodes.reverse()
for node in adjacent_nodes[1:]:
node.print()
self.stat_removed_array(adjacent_nodes)
# statistics functions
def stat_init(self,list_length,algorithm,array_length):
'''
initializes the counters
and starts the stop watch
'''
type(self)._stat_init(list_length,algorithm,array_length)
@classmethod
def _stat_init(cls,list_length,algorithm,array_length):
cls.stat_curr_space=0
cls.stat_max_space=0
cls.stat_counter=0
cls.stat_max_array_length=0
cls.stat_array_length=array_length
cls.stat_algorithm=algorithm
cls.stat_list_length=list_length
cls.stat_start_time=time.time()
def print_title(self):
'''
prints the legend and the caption for the statistics values
'''
type(self).print_title()
@classmethod
def print_title(cls):
print(' {0:10s} {1:s}'.format('space','maximal number of array space for'))
print(' {0:10s} {1:s}'.format('', 'pointers to the list nodes, that'))
print(' {0:10s} {1:s}'.format('', 'is needed'))
print(' {0:10s} {1:s}'.format('time', 'number of times the method next_node,'))
print(' {0:10s} {1:s}'.format('', 'that retrievs the successor of a node,'))
print(' {0:10s} {1:s}'.format('', 'was called'))
print(' {0:10s} {1:s}'.format('alg', 'algorithm that was selected:'))
print(' {0:10s} {1:s}'.format('', '0: array size is 2'))
print(' {0:10s} {1:s}'.format('', '1: array size is n, naive algorithm'))
print(' {0:10s} {1:s}'.format('', 't>1: array size is n^(1/t)'))
print(' {0:10s} {1:s}'.format('arr', 'dimension of the arrays'))
print(' {0:10s} {1:s}'.format('sz', 'actual maximal dimension of the arrays'))
print(' {0:10s} {1:s}'.format('n', 'list length'))
print(' {0:10s} {1:s}'.format('log', 'the logarithm to base 2 of n'))
print(' {0:10s} {1:s}'.format('n log n', 'n times the logarithm to base 2 of n'))
print(' {0:10s} {1:s}'.format('seconds', 'the runtime of the program in seconds'))
print()
print('{0:>10s} {1:>10s} {2:>4s} {3:>10s} {4:>10s} {5:>10s} {6:>5s} {7:>10s} {8:>10s}'
.format('space','time','alg','arr','sz','n','log', 'n log n','seconds'))
@classmethod
def print_statistic(cls):
'''
stops the stop watch and prints the statistics for the gathered counters
'''
run_time=time.time()-cls.stat_start_time
print('{0:10d} {1:10d} {2:4d} {3:10d} {4:10d} {5:10d} {6:5d} {7:10d} {8:10.2f}'.format(
cls.stat_max_space,cls.stat_counter,cls.stat_algorithm,
cls.stat_array_length,cls.stat_max_array_length,cls.stat_list_length,
int(math.log2(cls.stat_list_length)),int(cls.stat_list_length*math.log2(cls.stat_list_length)),
run_time
))
def stat_called_next_node(self):
'''
counter: should be called
if the next node funtion is called
'''
type(self)._stat_called_next_node()
@classmethod
def _stat_called_next_node(cls):
cls.stat_counter+=1
def stat_created_array(self,array):
'''
counter: should be called
after an array was created and filled
'''
type(self)._stat_created_array(array)
@classmethod
def _stat_created_array(cls,array):
cls.stat_curr_space+=len(array)
if cls.stat_curr_space> cls.stat_max_space:
cls.stat_max_space=cls.stat_curr_space
if (len(array)>cls.stat_max_array_length):
cls.stat_max_array_length=len(array)
def stat_removed_array(self,array):
'''
counter: should be called
before an array can be removed
'''
type(self)._stat_removed_array(array)
@classmethod
def _stat_removed_array(cls,array):
cls.stat_curr_space-=len(array)
@classmethod
def create_testlist(nodeclass, n):
'''
creates a single linked list of
n elements with values 1,...,n
'''
first_node=nodeclass(n,None)
for i in range(n-1,0,-1):
second_node=first_node
first_node=nodeclass(i,second_node)
return(first_node)
if __name__ == "__main__":
#cProfile.run('LinkedListNode.create_testlist(n).reverse_print()')
n=100000
ll=LinkedListNode.create_testlist(n)
LinkedListNode.do_print=False
ll.print_title()
ll.reverse_print(1)
ll.reverse_print(2)
ll.reverse_print(3)
ll.reverse_print(4)
ll.reverse_print(5)
ll.reverse_print(6)
ll.reverse_print(7)
ll.reverse_print(0)
以下是一些结果
space maximal number of array space for
pointers to the list nodes, that
is needed
time number of times the method next_node,
that retrievs the successor of a node,
was called
alg algorithm that was selected:
0: array size is 2
1: array size is n, naive algorithm
t>1: array size is n^(1/t)
arr dimension of the arrays
sz actual maximal dimension of the arrays
n list length
log the logarithm to base 2 of n
n log n n times the logarithm to base 2 of n
seconds the runtime of the program in seconds
space time alg arr sz n log n log n seconds
100000 100000 1 100000 100000 100000 16 1660964 0.17
635 200316 2 317 318 100000 16 1660964 0.30
143 302254 3 47 48 100000 16 1660964 0.44
75 546625 4 18 19 100000 16 1660964 0.99
56 515989 5 11 12 100000 16 1660964 0.78
47 752976 6 7 8 100000 16 1660964 1.33
45 747059 7 6 7 100000 16 1660964 1.23
54 1847062 0 2 3 100000 16 1660964 3.02
和
space maximal number of array space for
pointers to the list nodes, that
is needed
time number of times the method next_node,
that retrievs the successor of a node,
was called
alg algorithm that was selected:
0: array size is 2
1: array size is n, naive algorithm
t>1: array size is n^(1/t)
arr dimension of the arrays
sz actual maximal dimension of the arrays
n list length
log the logarithm to base 2 of n
n log n n times the logarithm to base 2 of n
seconds the runtime of the program in seconds
space time alg arr sz n log n log n seconds
1000000 1000000 1 1000000 1000000 1000000 19 19931568 1.73
2001 3499499 2 1000 1001 1000000 19 19931568 7.30
302 4514700 3 100 101 1000000 19 19931568 8.58
131 4033821 4 32 33 1000000 19 19931568 5.69
84 6452300 5 16 17 1000000 19 19931568 11.04
65 7623105 6 10 11 1000000 19 19931568 13.26
59 7295952 7 8 9 1000000 19 19931568 11.07
63 21776637 0 2 3 1000000 19 19931568 34.39
答案 1 :(得分:5)
就此问题的空间/时间要求而言,频谱有两端:
由于你不关心O(n)空间解决方案,让我们看看另一个:
def reverse_print(LL):
length = 0
curr = LL
while curr:
length += 1
curr = curr.next
for i in range(length, 0, -1):
curr = LL
for _ in range(i):
curr = curr.next
print(curr.value)
当然,如果您选择将其转换为双向链接列表,您可以在O(n)时间和0空格中执行此操作
答案 2 :(得分:2)
渴望发表评论:
OP中算法的运行时间不是O(n)。它是O(n log(n))。作为运行时间,我们定义了我们对节点的下一个节点进行测试的次数。这在方法reverse_print的主体中的3个位置处明确地完成。实际上它是在5个地方完成的:while-clause中的2个和while looop中的3个,但如果一个临时保存值,它可以减少到3个。 while循环重复约n / 2次。因此,reverse_print方法明确地获取下一个节点3/2 * 2次。它在while循环之后的reverse_print的两次调用中隐含地获取它们。要在这些调用中处理的列表长度是用于原始调用reverse_print的列表长度的一半,因此它是n / 2。因此,我们对运行时间有以下近似值:
t(n) = 1.5n+2t(n/2)
这种复发的解决方案是
t(n) = 1.5n log(n) + n
如果您将解决方案插入到reccurence中,则可以验证这一点。
您还可以运行问题计算获取节点的频率。为此我向你的程序添加了一个next_node()方法。我使用cProfiler来计算函数调用。我还添加了一个类方法来创建测试列表。最后以这个程序结束
import cProfile
import math
class LinkedListNode:
def __init__(self, value, next_node):
self.value = value
self._next_node = next_node
def next_node(self):
''' fetch the next node'''
return(self._next_node)
def reverse_print(self, list_tail):
list_head=self
if not self:
return
if not self.next_node():
print (self.value)
return
if self == list_tail:
print (self.value)
return
p0 = self
p1 = self
#assert(p1.next_node != list_tail)
p1_next=p1.next_node()
p1_next_next=p1_next.next_node()
while p1_next != list_tail and p1_next_next != list_tail:
p1 = p1_next_next
p0 = p0.next_node()
p1_next=p1.next_node()
if p1_next != list_tail:
p1_next_next=p1_next.next_node()
p0.next_node().reverse_print(list_tail)
self.reverse_print(p0)
@classmethod
def create_testlist(nodeclass, n):
''' creates a list of n elements with values 1,...,n'''
first_node=nodeclass(n,None)
for i in range(n-1,0,-1):
second_node=first_node
first_node=nodeclass(i,second_node)
return(first_node)
if __name__ == "__main__":
n=1000
cProfile.run('LinkedListNode.create_testlist(n).reverse_print(None)')
print('estimated number of calls of next_node',1.5*n*math.log(n,2)+n)
我得到了以下输出(最后是探查器的输出,显示了函数调用的数量):
>>>
RESTART: print_reversed_list2.py
1000
999
998
...
2
1
116221 function calls (114223 primitive calls) in 2.539 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 2.539 2.539 <string>:1(<module>)
2000 0.015 0.000 2.524 0.001 PyShell.py:1335(write)
1999/1 0.008 0.000 2.538 2.538 print_reversed_list2.py:12(reverse_print)
1 0.000 0.000 0.001 0.001 print_reversed_list2.py:36(create_testlist)
1000 0.000 0.000 0.000 0.000 print_reversed_list2.py:5(__init__)
16410 0.002 0.000 0.002 0.000 print_reversed_list2.py:9(next_node)
...
estimated number of calls of next_node 15948.67642699313
因此,公式估计的next_node()调用的数量约为15949. next_node()调用的实际数量是16410.后一个数字包括行p0.next_node().reverse_print(list_tail)
的next_node()的2000次调用我没有考虑我的公式。
因此,1.5*n*log(n)+n
似乎是对您的计划运行时间的合理估计。
答案 3 :(得分:-1)
免责声明:我错过了在本次讨论中无法修改列表。
想法:我们按正向顺序遍历列表,在我们处于此状态时将其反转。当我们到达终点时,我们向后迭代,打印元素并再次反转列表 核心观察是你可以就地反转一个列表:你需要的只是记住你最后一个元素。
未经测试,丑陋的伪代码:
def printReverse(list) {
prev = nil
cur = list.head
if cur == nil {
return
}
while cur != nil {
next = cur.next
// [prev] cur -> next
cur.next = prev
// [prev] <- cur next
prev = cur
// [____] <- prev next
cur = next
// [____] <- prev cur
}
// Now cur is nil and prev the last element!
cur = prev
prev = nil
while cur != nil {
print cur
// Rewire just as above:
next = cur.next
cur.next = prev
prev = cur
cur = next
}
}
显然,它在时间O(n)中运行,只占用O(1)(附加)空间(三个本地指针/引用变量)。