簡體   English   中英

反向打印一個小於O(n)空間的不可變鏈表

[英]reverse print an immutable linked list with less than O(n) space

解決這個問題,我的想法是遞歸的,在每次遞歸過程中,反向打印鏈表的下半部分,然后反向打印鏈表的上半部分。 因此額外的空間是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)

這是O(n)時間和O(sqrt(n))空間算法。 在帖子的第二部分,它將擴展到線性時間和O(n ^(1 / t))空間算法,用於任意正整數t。

高級想法:將列表拆分為sqrt(n)許多(幾乎)相等大小的部分。 使用從最后到第一個的朴素線性時間線性空間方法以相反的順序依次打印部件。

要存儲部件的起始節點,我們需要一個大小為O(sqrt(n))的數組。 要恢復大小約為sqrt(n)的部分,朴素算法需要一個數組來存儲對部件節點的引用。 所以數組的大小為O(sqrt(n)。


一個使用大小為k=[sqrt(n)]+1 =O(sqrt(n))兩個數組( lsassa )(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. O(n)空間,O(n)時間
  2. O(1)空間,O(n ^ 2)時間

既然你不關心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空間中執行此操作

渴望評論:

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.send_node()調用的實際數量是16410.后一個數字包括行p0.next_node().reverse_print(list_tail)的next_node()的2000次調用.reverse_print p0.next_node().reverse_print(list_tail)我沒有考慮我的公式。

因此, 1.5*n*log(n)+n似乎是對程序運行時間的合理估計。

免責聲明:我錯過了在本次討論的背景下無法修改列表。


想法:我們按正向順序迭代列表,在我們處於此狀態時將其反轉。 當我們到達終點時,我們向后迭代,打印元素並再次反轉列表。
核心觀察是你可以就地反轉一個列表:你需要的只是記住你最后一個元素。

未經測試,丑陋的偽代碼:

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)(附加)空間(三個本地指針/引用變量)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM