[软件设计/软件工程] 单链表 C++ 的快速选择算法

[复制链接]
发表于 2022-5-5 09:21:19
问题
我需要一种算法,它可以找到具有线性时间复杂度 O(n) 和恒定空间复杂度 O(1) 的单链表的中位数。

编辑:单链表是 C 风格的单链表。不允许使用 stl(没有容器,没有函数,禁止所有 stl,例如没有 std::forward_list)。不允许在任何其他容器(例如数组)中移动数字。

O(logn) 的空间复杂度是可以接受的,因为我的列表实际上小于 100。另外,我不允许使用像 nth_element 这样的 STL 函数

基本上,我有一个包含 3*10^6 元素的链表,我需要在 3 秒内得到中位数,所以我不能用排序算法对列表进行排序(它会是 O(nlogn),它可能需要 10-14 秒)。

我在网上做了一些搜索,发现 std::vector 与 quickselect 的中位数可以在 O(n) 和 O(1) 空间中找到(最坏的情况是 O(n^2),但很少看到),例如:

但是我找不到任何算法来为链表执行此操作。问题是,我可以使用数组索引随机访问向量,如果我想修改算法,复杂度会大得多,因为。例如,当我将枢轴索引更改为左时,我实际上需要遍历列表以获取新元素并走得更远(这将使我的列表至少 O(k n) 和一个大 k,甚至 O(n^ 2)…)。

编辑2:

我知道我有太多变量,但我一直在测试不同的东西,我仍然在我的代码中。 . .

我当前的代码:
  1. #include <bits/stdc++.h>

  2. using namespace std;

  3. template <class T> class Node {
  4.     public:
  5.     T data;
  6.     Node<T> *next;
  7. };

  8. template <class T> class List {
  9.     public:
  10.     Node<T> *first;
  11. };

  12. template <class T> T getMedianValue(List<T> & l) {
  13.     Node<T> *crt,*pivot,*incpivot;
  14.     int left, right, lung, idx, lungrel,lungrel2, left2, right2, aux, offset;
  15.     pivot = l.first;
  16.     crt = pivot->next;
  17.     lung = 1;
  18. //lung is the lenght of the linked list (yeah it's lenght in romanian...)
  19. //lungrel and lungrel2 are the relative lenghts of the part of
  20. //the list I am processing, e.g: 2 3 4 in a list with 1 2 3 4 5
  21.     right = left = 0;
  22.     while (crt != NULL) {
  23.         if(crt->data < pivot->data){
  24.             aux = pivot->data;
  25.             pivot->data = crt->data;
  26.             crt->data = pivot->next->data;
  27.             pivot->next->data = aux;
  28.             pivot = pivot->next;
  29.             left++;
  30.         }
  31.         else right++;
  32.        // cout<<crt->data<<endl;
  33.         crt = crt->next;
  34.         lung++;
  35.     }
  36.     if(right > left) offset = left;
  37. //  cout<<endl;
  38. //  cout<<pivot->data<<" "<<left<<" "<<right<<endl;
  39. //  printList(l);
  40. //  cout<<endl;
  41.     lungrel = lung;
  42.     incpivot = l.first;
  43.    // offset = 0;
  44.     while(left != right){
  45.         //cout<<"parcurgere"<<endl;
  46.         if(left > right){
  47.             //cout<<endl;
  48.             //printList(l);
  49.             //cout<<endl;
  50.             //cout<<"testleft "<<incpivot->data<<" "<<left<<" "<<right<<endl;
  51.             crt = incpivot->next;
  52.             pivot = incpivot;
  53.             idx = offset;left2 = right2 = lungrel = 0;
  54.             //cout<<idx<<endl;
  55.             while(idx < left && crt!=NULL){
  56.                  if(pivot->data > crt->data){
  57.                    //  cout<<"1crt "<<crt->data<<endl;
  58.                      aux = pivot->data;
  59.                      pivot->data = crt->data;
  60.                      crt->data = pivot->next->data;
  61.                      pivot->next->data = aux;
  62.                      pivot = pivot->next;
  63.                      left2++;lungrel++;
  64.                   }
  65.                   else {
  66.                       right2++;lungrel++;
  67.                       //cout<<crt->data<<" "<<right2<<endl;
  68.                   }
  69.                   //cout<<crt->data<<endl;
  70.                   crt = crt->next;
  71.                   idx++;
  72.              }
  73.              left = left2 + offset;
  74.              right = lung - left - 1;
  75.              if(right > left) offset = left;
  76.              //if(pivot->data == 18) return 18;
  77.              //cout<<endl;
  78.              //cout<<"l "<<pivot->data<<" "<<left<<" "<<right<<" "<<right2<<endl;
  79.            //  printList(l);
  80.         }
  81.         else if(left < right && pivot->next!=NULL){
  82.             idx = left;left2 = right2 = 0;
  83.             incpivot = pivot->next;offset++;left++;
  84.             //cout<<endl;
  85.             //printList(l);
  86.             //cout<<endl;
  87.             //cout<<"testright "<<incpivot->data<<" "<<left<<" "<<right<<endl;
  88.             pivot = pivot->next;
  89.             crt = pivot->next;
  90.             lungrel2 = lungrel;
  91.             lungrel = 0;
  92.            // cout<<"p right"<<pivot->data<<" "<<left<<" "<<right<<endl;
  93.             while((idx < lungrel2 + offset - 1) && crt!=NULL){
  94.                  if(crt->data < pivot->data){
  95.                 //     cout<<"crt "<<crt->data<<endl;
  96.                      aux = pivot->data;
  97.                      pivot->data = crt->data;
  98.                      crt->data = (pivot->next)->data;
  99.                      (pivot->next)->data = aux;
  100.                      pivot = pivot->next;
  101.                  //    cout<<"crt2 "<<crt->data<<endl;
  102.                      left2++;lungrel++;
  103.                   }
  104.                   else right2++;lungrel++;
  105.                   //cout<<crt->data<<endl;
  106.                   crt = crt->next;
  107.                   idx++;
  108.              }
  109.              left = left2 + left;
  110.              right = lung - left - 1;
  111.                  if(right > left) offset = left;
  112.             // cout<<"r "<<pivot->data<<" "<<left<<" "<<right<<endl;
  113.            //  printList(l);
  114.         }
  115.         else{
  116.             //cout<<cmx<<endl;
  117.             return pivot->data;
  118.         }
  119.     }
  120.     //cout<<cmx<<endl;
  121.     return pivot->data;
  122. }
  123. template <class T> void printList(List<T> const & l) {
  124.     Node<T> *tmp;
  125.     if(l.first != NULL){
  126.         tmp = l.first;
  127.         while(tmp != NULL){
  128.             cout<<tmp->data<<" ";
  129.             tmp = tmp->next;
  130.         }
  131.     }
  132. }
  133. template <class T> void push_front(List<T> & l, int x)
  134. {
  135.     Node<T>* tmp = new Node<T>;

  136.     tmp->data = x;

  137.     tmp->next = l.first;
  138.     l.first = tmp;
  139. }

  140. int main(){
  141.     List<int> l;
  142.     int n = 0;
  143.     push_front(l, 19);
  144.     push_front(l, 12);
  145.     push_front(l, 11);
  146.     push_front(l, 101);
  147.     push_front(l, 91);
  148.     push_front(l, 21);
  149.     push_front(l, 9);
  150.     push_front(l, 6);
  151.     push_front(l, 25);
  152.     push_front(l, 4);
  153.     push_front(l, 18);
  154.     push_front(l, 2);
  155.     push_front(l, 8);
  156.     push_front(l, 10);
  157.     push_front(l, 200);
  158.     push_front(l, 225);
  159.     push_front(l, 170);
  160.     printList(l);
  161.     n=getMedianValue(l);
  162.     cout<<endl;
  163.     cout<<n;

  164.     return 0;
  165. }
复制代码

您对如何使快速选择适应单独列出的链接或其他可以解决我的问题的算法有任何建议吗?

回答
在您的问题中,您提到将枢轴点向左移动时遇到问题,因为这需要遍历列表。如果你做对了,你只需要遍历整个列表两次:

如果你不太在意选择一个好的枢轴,只是喜欢选择列表中的第一个元素作为枢轴(如果数据已经排序,这将导致最坏情况 O(n^ 2)时间复杂度),不需要第一步。

如果第一次遍历链表的尾端,通过保持指向链表尾的指针,就不必再次遍历它来找到尾了。此外,如果您使用标准 Lomuto 分区方案(我不这样做,原因如下所述),您还必须在列表中保留两个指针,表示标准 Lomuto 分区方案的 i 和 j 索引.通过使用这些指针,您永远不必遍历列表来访问单个元素。

此外,如果您保留一个指向每个分区的中间和结尾的指针,那么当您稍后必须对其中一个分区进行排序时,您不必再次遍历该分区来找到中间和结尾。

我现在已经为链表创建了我自己的 QuickSelect 算法实现,我已经在下面发布了。

既然你说链表是单链表,不能升级成双链表,那我就不能用Hoare分区方案了,因为反向迭代单链表代价很大。所以我正在使用通常效率较低的 Lomuto 分区方案。

在使用 Lomuto 分区方案时,通常选择第一个元素或最后一个元素作为轴。然而,选择这两种方法中的任何一种都有一个缺点,即排序数据将导致算法的最坏情况时间复杂度为 O(n^2)。这可以通过根据“三的中值”选择轴来防止。规则,即从第一个元素、中间元素和最后一个元素的中值中选择轴。所以在我的实现中,我使用这个“三位数”。规则。

此外,Lomuto 分区方案通常会创建两个分区,一个用于小于轴的值,一个用于大于或等于轴的值。但是,如果所有值都相同,这将导致 O(n^2) 的最坏情况时间复杂度。所以,在我的实现中,我创建了三个分区,一个用于小于轴的值,一个用于大于轴的值,一个用于等于轴的值。

虽然这些措施并没有完全消除 O(n^2) 的最坏情况时间复杂度的可能性,但它们至少使其极不可能发生。

我遇到的一个重要问题是,对于偶数个元素,中位数被定义为两个“中间”元素的算术平均值。或“中间”元素。所以我不能简单地调用函数 find_nth_element 因为例如,如果元素的总数是 14,那么我将寻找第 7 和第 8 个最大的元素。这意味着我将不得不两次调用这样的函数,这将是低效的。所以我写了一个搜索“中位数”的函数。同时元素。尽管这使代码更加复杂,但与不必调用相同函数两次的优势相比,由于额外代码复杂性导致的性能损失应该是最小的。

请注意,虽然我的实现完全在 C++ 编译器上编译,但我不会将其称为教科书 C++ 代码,因为它禁止使用 C++ 标准模板库中的任何内容。所以我的代码是 C 代码和 C++ 代码的混合体。
  1. #include <iostream>
  2. #include <iomanip>
  3. #include <cassert>

  4. //remove the comment in the following line for extra debugging information
  5. //#define PRINT_DEBUG_INFO

  6. template <typename T>
  7. struct Node
  8. {
  9.     T data;
  10.     Node<T> *next;
  11. };

  12. // NOTE:
  13. // The return type is not necessarily the same as the data type. The reason for this is
  14. // that, for example, the data type "int" requires a "double" as a return type, so that
  15. // the arithmetic mean of "3" and "6" returns "4.5".
  16. // This function may require template specializations to handle overflow or wrapping.
  17. template<typename T, typename U>
  18. U arithmetic_mean( const T &first, const T &second )
  19. {
  20.     return ( static_cast<U>(first) + static_cast<U>(second) ) / 2;
  21. }

  22. //the main loop of the function find_median can be in one of the following three states
  23. enum LoopState
  24. {
  25.     //we are looking for one median value
  26.     LOOPSTATE_LOOKINGFORONE,

  27.     //we are looking for two median values, and the returned median
  28.     //will be the arithmetic mean of the two
  29.     LOOPSTATE_LOOKINGFORTWO,

  30.     //one of the median values has been found, but we are still searching for
  31.     //the second one
  32.     LOOPSTATE_FOUNDONE
  33. };

  34. template <
  35.     typename T, //type of the data
  36.     typename U  //type of the return value
  37. >
  38. U find_median( Node<T> *list )
  39. {
  40.     //This variable points to the pointer to the first element of the current partition.
  41.     //During the partition phase, the linked list will be broken and reassembled afterwards, so
  42.     //the pointer this pointer points to will be nullptr until it is reassembled.
  43.     Node<T> **pp_start = &list;

  44.     //these pointer are maintained for accessing the middle of the list for selecting a pivot using
  45.     //the "median-of-three" rule
  46.     Node<T> *p_middle;
  47.     Node<T> *p_end;

  48.     //result is not defined if list is empty
  49.     assert( *pp_start != nullptr );

  50.     //in the main loop, this variable always holds the number of elements in the current partition
  51.     int num_total = 1;

  52.     // First, we must traverse the entire linked list in order to determine the number of elements,
  53.     // in order to calculate k1 and k2. If it is odd, then the median is defined as the k'th smallest
  54.     // element where k = n / 2. If the number of elements is even, then the median is defined as the
  55.     // arithmetic mean of the k'th element and the (k+1)'th element.
  56.     // We also set a pointer to the nodes in the middle and at the end, which will be required later
  57.     // for selecting a pivot according to the "median-of-three" rule.
  58.     p_middle = *pp_start;
  59.     for ( p_end = *pp_start; p_end->next != nullptr; p_end = p_end->next )
  60.     {
  61.         num_total++;
  62.         if ( num_total % 2 == 0 ) p_middle = p_middle->next;
  63.     }   

  64.     // find out whether we are looking for only one or two median values
  65.     enum LoopState loop_state = num_total % 2 == 0 ? LOOPSTATE_LOOKINGFORTWO : LOOPSTATE_LOOKINGFORONE;

  66.     //set k to the index of the middle element, or if there are two middle elements, to the left one
  67.     int k = ( num_total - 1 ) / 2;

  68.     // If we are looking for two median values, but we have only found one, then this variable will
  69.     // hold the value of the one we found. Whether we have found one can be determined by the state of
  70.     // the variable loop_state.
  71.     T val_found;

  72.     for (;;)
  73.     {
  74.         assert( *pp_start != nullptr );
  75.         assert( p_middle  != nullptr );
  76.         assert( p_end     != nullptr );
  77.         assert( num_total != 0 );

  78.         if ( num_total == 1 )
  79.         {
  80.             switch ( loop_state )
  81.             {
  82.             case LOOPSTATE_LOOKINGFORONE:
  83.                 return (*pp_start)->data;
  84.             case LOOPSTATE_FOUNDONE:
  85.                 return arithmetic_mean<T,U>( val_found, (*pp_start)->data );
  86.             default:
  87.                 assert( false ); //this should be unreachable
  88.             }
  89.         }

  90.         //select the pivot according to the "median-of-three" rule
  91.         T pivot;
  92.         if ( (*pp_start)->data < p_middle->data )
  93.         {
  94.             if ( p_middle->data < p_end->data )
  95.                 pivot = p_middle->data;
  96.             else if ( (*pp_start)->data < p_end->data )
  97.                 pivot = p_end->data;
  98.             else
  99.                 pivot = (*pp_start)->data;
  100.         }
  101.         else
  102.         {
  103.             if ( (*pp_start)->data < p_end->data )
  104.                 pivot = (*pp_start)->data;
  105.             else if ( p_middle->data < p_end->data )
  106.                 pivot = p_end->data;
  107.             else
  108.                 pivot = p_middle->data;
  109.         }


  110.         // We will be dividing the current partition into 3 new partitions (less-than,
  111.         // equal-to and greater-than) each represented as a linked list. Each list
  112.         // requires a pointer to the start of the list and a pointer to the pointer at
  113.         // the end of the list to write the address of new elements to. Also, when
  114.         // traversing the lists, we need to keep a pointer to the middle of the list,
  115.         // as this information will be required for selecting a new pivot in the next
  116.         // iteration of the loop. The latter is not required for the equal-to partition,
  117.         // as it would never be used.
  118.         Node<T> *p_less    = nullptr, **pp_less_end    = &p_less,    **pp_less_middle    = &p_less;
  119.         Node<T> *p_equal   = nullptr, **pp_equal_end   = &p_equal;
  120.         Node<T> *p_greater = nullptr, **pp_greater_end = &p_greater, **pp_greater_middle = &p_greater;

  121.         // These pointers are only used as a cache to the location of end node. Despite
  122.         // their similar name, their function is very different to pp_less_end and
  123.         // pp_greater_end.
  124.         Node<T> *p_less_end    = nullptr;
  125.         Node<T> *p_greater_end = nullptr;

  126.         // counter for the number of elements in each partition
  127.         int num_less = 0;
  128.         int num_equal = 0;
  129.         int num_greater = 0;

  130.         // NOTE:
  131.         // The following loop will temporarily split the linked list. It will be merged later.
  132.         Node<T> *p_next_node = *pp_start;
  133.         *pp_start = nullptr;
  134.         for ( int a = 0; a < num_total; a++ )
  135.         {
  136.             assert( p_next_node != nullptr );

  137.             Node<T> *p_current_node = p_next_node;
  138.             p_next_node = p_next_node->next;

  139.             if ( p_current_node->data < pivot )
  140.             {
  141.                 //link node to pp_less
  142.                 assert( *pp_less_end == nullptr );
  143.                 *pp_less_end = p_current_node;
  144.                 pp_less_end = &p_current_node->next;
  145.                 p_current_node->next = nullptr;

  146.                 num_less++;
  147.                 if ( num_less % 2 == 0 )
  148.                 {
  149.                     pp_less_middle = &(*pp_less_middle)->next;
  150.                 }

  151.                 //setting this variable is only done for caching purposes and is not
  152.                 //directly related to the logic of the other variable pp_less_end
  153.                 p_less_end = p_current_node;
  154.             }
  155.             else if ( p_current_node->data == pivot )
  156.             {
  157.                 //link node to pp_equal
  158.                 assert( *pp_equal_end == nullptr );
  159.                 *pp_equal_end = p_current_node;
  160.                 pp_equal_end = &p_current_node->next;
  161.                 p_current_node->next = nullptr;

  162.                 num_equal++;
  163.             }
  164.             else
  165.             {
  166.                 //link node to pp_greater
  167.                 assert( *pp_greater_end == nullptr );
  168.                 *pp_greater_end = p_current_node;
  169.                 pp_greater_end = &p_current_node->next;
  170.                 p_current_node->next = nullptr;

  171.                 num_greater++;
  172.                 if ( num_greater % 2 == 0 )
  173.                 {
  174.                     pp_greater_middle = &(*pp_greater_middle)->next;
  175.                 }

  176.                 //setting this variable is only done for caching purposes and is not
  177.                 //directly related to the logic of the other variable pp_greater_end
  178.                 p_greater_end = p_current_node;
  179.             }
  180.         }

  181.         assert( num_total == num_less + num_equal + num_greater );

  182. #ifdef PRINT_DEBUG_INFO
  183.         //when PRINT_DEBUG_INFO is defined, it will print the length of all partitions and their contents
  184.         {
  185.             Node<T> *p;
  186.             std::cout << std::setfill( '0' );
  187.             std::cout << "partition lengths: ";
  188.             std::cout <<
  189.                 std::setw( 2 ) << num_less    << " " <<
  190.                 std::setw( 2 ) << num_equal   << " " <<
  191.                 std::setw( 2 ) << num_greater << " " <<
  192.                 std::setw( 2 ) << num_total   << "\n";
  193.             std::cout << "less: ";
  194.             for ( p = p_less; p != nullptr; p = p->next ) std::cout << p->data << " ";
  195.             std::cout << "\nequal: ";
  196.             for ( p = p_equal; p != nullptr; p = p->next ) std::cout << p->data << " ";
  197.             std::cout << "\ngreater: ";
  198.             for ( p = p_greater; p != nullptr; p = p->next ) std::cout << p->data << " ";
  199.             std::cout << "\n\n" << std::flush;
  200.         }
  201. #endif

  202.         //insert less-than partition into list
  203.         assert( *pp_start == nullptr );
  204.         *pp_start = p_less;

  205.         //insert equal-to partition into list
  206.         assert( *pp_less_end == nullptr );
  207.         *pp_less_end = p_equal;

  208.         //insert greater-than partition into list
  209.         assert( *pp_equal_end == nullptr );
  210.         *pp_equal_end = p_greater;

  211.         //link list to previously cut off part
  212.         assert( *pp_greater_end == nullptr );
  213.         *pp_greater_end = p_next_node;

  214.         //if less-than partition is large enough to hold both possible median values
  215.         if ( k + 2 <= num_less )
  216.         {
  217.             //set the next iteration of the loop to process the less-than partition
  218.             //pp_start is already set to the desired value
  219.             p_middle = *pp_less_middle;
  220.             p_end = p_less_end;
  221.             num_total = num_less;
  222.         }

  223.         //else if less-than partition holds one of both possible median values
  224.         else if ( k + 1 == num_less )
  225.         {
  226.             if ( loop_state == LOOPSTATE_LOOKINGFORTWO )
  227.             {
  228.                 //the equal_to partition never needs sorting, because all members are already equal
  229.                 val_found = p_equal->data;
  230.                 loop_state = LOOPSTATE_FOUNDONE;
  231.             }
  232.             //set the next iteration of the loop to process the less-than partition
  233.             //pp_start is already set to the desired value
  234.             p_middle = *pp_less_middle;
  235.             p_end = p_less_end;
  236.             num_total = num_less;
  237.         }

  238.         //else if equal-to partition holds both possible median values
  239.         else if ( k + 2 <= num_less + num_equal )
  240.         {
  241.             //the equal_to partition never needs sorting, because all members are already equal
  242.             if ( loop_state == LOOPSTATE_FOUNDONE )
  243.                 return arithmetic_mean<T,U>( val_found, p_equal->data );
  244.             return p_equal->data;
  245.         }

  246.         //else if equal-to partition holds one of both possible median values
  247.         else if ( k + 1 == num_less + num_equal )
  248.         {
  249.             switch ( loop_state )
  250.             {
  251.             case LOOPSTATE_LOOKINGFORONE:
  252.                 return p_equal->data;
  253.             case LOOPSTATE_LOOKINGFORTWO:
  254.                 val_found = p_equal->data;
  255.                 loop_state = LOOPSTATE_FOUNDONE;
  256.                 k = 0;
  257.                 //set the next iteration of the loop to process the greater-than partition
  258.                 pp_start = pp_equal_end;
  259.                 p_middle = *pp_greater_middle;
  260.                 p_end = p_greater_end;
  261.                 num_total = num_greater;
  262.                 break;
  263.             case LOOPSTATE_FOUNDONE:
  264.                 return arithmetic_mean<T,U>( val_found, p_equal->data );
  265.             }
  266.         }

  267.         //else both possible median values must be in the greater-than partition
  268.         else
  269.         {
  270.             k = k - num_less - num_equal;

  271.             //set the next iteration of the loop to process the greater-than partition
  272.             pp_start = pp_equal_end;
  273.             p_middle = *pp_greater_middle;
  274.             p_end = p_greater_end;
  275.             num_total = num_greater;
  276.         }
  277.     }
  278. }


  279. // NOTE:
  280. // The following code is not part of the algorithm, but is only intended to test the algorithm

  281. template <typename T>
  282. class List
  283. {
  284. public:
  285.     List() : first( nullptr ) {}

  286.     // the following is required to abide by the rule of three/five/zero
  287.     // see: https://en.cppreference.com/w/cpp/language/rule_of_three
  288.     List( const List<T> & ) = delete;
  289.     List( const List<T> && ) = delete;
  290.     List<T>& operator=( List<T> & ) = delete;
  291.     List<T>& operator=( List<T> && ) = delete;

  292.     ~List()
  293.     {
  294.         Node<T> *p = first;

  295.         while ( p != nullptr )
  296.         {
  297.             Node<T> *temp = p;
  298.             p = p->next;
  299.             delete temp;
  300.         }
  301.     }

  302.     void push_front( int data )
  303.     {
  304.         Node<T>* tmp = new Node<T>;

  305.         tmp->data = data;

  306.         tmp->next = first;
  307.         first = tmp;
  308.     }

  309.     //member variables
  310.     Node<T> *first;
  311. };

  312. int main()
  313. {
  314.     List<int> l;

  315.     int unsorted_data[] = { 6, 19, 3, 7, 4, 9, 8, 2, 18, 4, 10, 11, 12, 10 };

  316.     //create singly-linked list
  317.     for ( int i = 0; i < sizeof( unsorted_data ) / sizeof( *unsorted_data ); i++ ) l.push_front( unsorted_data[i] );

  318.     std::cout << "The median is: " << std::setprecision(10) << find_median<int,double>( l.first ) << std::endl;

  319.     return 0;
  320. }
复制代码

我已经用一百万个随机生成的元素成功地测试了我的代码,它几乎立即找到了正确的中位数。





上一篇:在android中合并两个png文件
下一篇:使用 puppeter 打开本地 HTML 文件

使用道具 举报

Archiver|手机版|小黑屋|吾爱开源 |网站地图

Copyright 2011 - 2012 Lnqq.NET.All Rights Reserved( ICP备案粤ICP备14042591号-1粤ICP14042591号 )

关于本站 - 版权申明 - 侵删联系 - Ln Studio! - 广告联系

本站资源来自互联网,仅供用户测试使用,相关版权归原作者所有

快速回复 返回顶部 返回列表