【问题标题】:Sorting a linked array对链接数组进行排序
【发布时间】:2021-12-09 23:20:22
【问题描述】:

上下文:

我有一个LinkedList 类,它包含并以正确的顺序自动插入节点。请注意,这是一个链表数据结构(保存节点/元素的数组代表 RAM,指针 - headtail 以及 nextprev 代表 RAM 中的地址(但在本例中,它们是保存节点的数组的索引)。

例如

myLinkedList.insert(2);
myLinkedList.insert(1);
myLinkedList.output(); // => [{value:2, next:null, prev:1}, {value:1,next:0,prev:null]}, head = 1, tail = 0

所以现在当我调用我的printInOrder 函数时,它会输出1,然后是2,然后停止。

注意:当我插入一个新节点时,它会被推到数组的末尾,但是它的相邻节点的指针会改变(所以nextprev ) 以便从headtail 的类似火车的路径包括以特定顺序排列的所有节点(默认为升序)。所以被插入的节点总是在最后,只有它的指针表示它的位置。

我的问题:

这是我的问题:(见问题末尾的代码)

假设我创建了一个链表,默认排序(升序),我的值是 2、1 和 3。所以当我遍历链表时,我会收到 1、2、3。现在,我想重新排序链表。这意味着,每个节点的索引不会改变,但节点的指针会改变。毕竟,指针是创建订单的东西。那么我将如何使用一些排序算法,比如合并或冒泡,对我的节点进行排序而不实际更改它们的顺序,只更改它们的 nextprev 指针(以及全局 headtail)。

我的代码

这里是目前重新排序功能的代码,目前使用冒泡排序但不起作用:

class LinkedList {
  constructor(sortingFunction) {
    this.head;
    this.tail;
    this.list = [];
    this.sortingFunction = sortingFunction ? ? ((a, b) => {
      return a < b
    });
  }
  sort(sortingFunction) {
    if (!sortingFunction) {
      return false;
    }
    this.head = null;
    this.tail = null;
    const arr = this.list.map(x => x);
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length; j++) {
        if (!arr[j + 1] ? .value) {
          console.log("no");
          continue;
        }
        if (sortingFunction(arr[j].value, arr[j + 1].value)) {
          let tmp_next = arr[j].next;
          let tmp_prev = arr[j].previous;
          arr[j].next = arr[j + 1].next;
          arr[j].previous = arr[j + 1].previous;
          arr[j + 1].next = tmp_next;
          arr[j + 1].previous = tmp_prev;
        }
      }
    }
    this.list = arr;
  }
}

这是我的LinkedList 类的完整代码,这将允许您重新创建我的问题 - 即节点根本不会自行排序。他们的指针改变了,但不是他们应该的方式,我不明白为什么。

class LinkedList {
   constructor(sortingFunction) {
      this.head;
      this.tail;
      this.list = [];
      this.sortingFunction = sortingFunction ?? ((a,b) => {return a < b});
   }

   some(func) {
      let currentNode = this.list[this.head];
      let index = this.head;
      while(!func(currentNode)) {
         index = currentNode.next;
         currentNode = this.list[index];
         if(index == undefined || index == null) { return -1; }
      }
      return index;
   }
   forEachInOrder(func) {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         func(node);
         current = node.next;
      }
   }
   * iterator() {
      let current = this.head;
      while(current != undefined) {
         const node = this.list[current];
         yield node;
         current = node.next;
      }
   }
   
   insert(value) {
      if(!this.list.length) {
         this.head = 0;
         this.tail = 0;
         this.list.push({value, next:null,previous:null});
         return 0;
      }
      let nodeToInsert = {value, next:null,previous:null};
      let indexToInsert = this.head;
      let nthnode = this.list[this.head];
      while(nthnode && this.sortingFunction(nthnode.value, value)) {
         indexToInsert = nthnode.next;
         nthnode = this.list[indexToInsert];
      }
      if(indexToInsert === null) {
         // new tail (biggest)
         nodeToInsert.previous = this.tail;
         this.list[this.tail].next = this.list.length;
         this.tail = this.list.length;
      } else if(indexToInsert === this.head) {
         // new head (smallest)
         nodeToInsert.next = this.head;
         this.list[this.head].previous = this.list.length;
         this.head = this.list.length;
      } else {
         nodeToInsert.next = indexToInsert;
         if(this.list[this.list[indexToInsert].previous]?.next != undefined) {
            this.list[this.list[indexToInsert].previous].next = this.list.length;
         }
         nodeToInsert.previous = this.list[indexToInsert].previous;
         this.list[indexToInsert].previous = this.list.length;
      }
      this.list.push(nodeToInsert);
      return 1;
   }

   reverse() {
      let _temp;
      for(let i = 0; i < this.list.length; i++) {
         _temp = this.list[i].next;
         this.list[i].next = this.list[i].previous;
         this.list[i].previous = _temp;
      }
      _temp = this.head;
      this.head = this.tail;
      this.tail = _temp;
   }
   sort(sortingFunction) {
      if(!sortingFunction) {return false;}
      this.head = null;
      this.tail = null;
      const arr = this.list.map(x=>x);
      for (let i = 0; i < arr.length; i++) {
         for (let j = 0; j < arr.length; j++) {
            if(!arr[j+1]?.value) {continue;}
            if (sortingFunction(arr[j].value, arr[j+1].value)) {
               let tmp_next = arr[j].next;
               let tmp_prev = arr[j].previous;
               arr[j].next = arr[j+1].next;
               arr[j].previous = arr[j+1].previous;
               arr[j+1].next = tmp_next;
               arr[j+1].previous = tmp_prev;
            }
         }
      }
      this.list = arr;
   }

   print() {
      console.log(this.list);
      console.log("Head:",this.head,"\nTail:",this.tail, "\nDefault is ascending order.");
   }
   printInOrder() {
      let current = this.list[this.head];
      while(current) {
         console.log(current.value);
         current = this.list[current.next];
      }
   }
}


const list = new LinkedList();

list.insert(100);
list.insert(30);
list.insert(50);
list.insert(400);
list.insert(10);
list.insert(200);
list.insert(-90);



console.log("When each node is sorted when it is inserted:")

list.print();

list.sort((a, b) => {
  return a > b;
});

console.log("Now, when re-sorted:");

list.print();

【问题讨论】:

    标签: javascript arrays sorting linked-list


    【解决方案1】:

    sort 函数中的一些问题:

    • 内部循环查看按索引连续的对,但它应该比较按链接连续的对,因为这是你应该决定交换的地方必要与否。

    • 您的代码交换涉及 4 个链接分配,但实际上涉及 6 个链接:

    • this.headthis.tail 设置为 null,但再也不会设置为相应的值。

    您的代码有一个很好的 iterator() 方法,我想在您的冒泡排序中重用它,但为了避免它使用最近交换已更改的 next 引用,我建议进行微小的更改在该生成器中,因此它将对此免疫:

       * iterator() {
          let current = this.head;
          while(current != undefined) {
             const node = this.list[current];
             current = node.next; // <--- move this here!
             yield node; // ...so the consumer can safely alter node.next
          }
       }
    

    现在你的sort 方法可以变成:

       sort(sortingFunction) {
          if (!sortingFunction) return;
          
          for (let i = 0; i < this.list.length; i++) {
             let prevNode = null;
             // Iterate in the current order, so by following the links:
             for (let currNode of this.iterator()) { // Requires the change in the generator 
                if (prevNode && sortingFunction(currNode.value, prevNode.value)) {
                   // Get the indexes of the current pair of nodes:
                   let currIndex = prevNode.next;
                   let prevIndex = currNode.previous;
                   // Update links from outer neighbors to these two nodes
                   if (currNode.next != null) this.list[currNode.next].previous = prevIndex;
                   else this.tail = prevIndex;
                   if (prevNode.previous != null) this.list[prevNode.previous].next = currIndex;
                   else this.head = currIndex;
                   // Update links from these two nodes to outer neighbors:
                   currNode.previous = prevNode.previous;
                   prevNode.next = currNode.next;
                   // Update links between the two nodes themselves:
                   prevNode.previous = currIndex;
                   currNode.next = prevIndex;
                } else {
                   prevNode = currNode;
                }
             }
          }
       }
    

    这是整个脚本,我无法抗拒在forEachInOrdersome 中使用你的生成器函数:

    class LinkedList {
       constructor(sortingFunction) {
          this.head;
          this.tail;
          this.list = [];
          this.sortingFunction = sortingFunction ?? ((a,b) => {return a < b});
       }
    
       some(func) {  // I adapted this to use the iterator
          for (const node of this.iterator()) {
             if (func(node)) {
                return node.previous == null ? this.head : this.list[node.previous].next;
             }
          }
          return -1;
       }
    
       forEachInOrder(func) { // I adapted this to use the iterator
          for (const node of this.iterator()) func(node);
       }
    
       * iterator() {
          let current = this.head;
          while(current != undefined) {
             const node = this.list[current];
             current = node.next; // <--- move this here!
             yield node;
          }
       }
       
       insert(value) {
          if(!this.list.length) {
             this.head = 0;
             this.tail = 0;
             this.list.push({value, next:null,previous:null});
             return 0;
          }
          let nodeToInsert = {value, next:null,previous:null};
          let indexToInsert = this.head;
          let nthnode = this.list[this.head];
          while(nthnode && this.sortingFunction(nthnode.value, value)) {
             indexToInsert = nthnode.next;
             nthnode = this.list[indexToInsert];
          }
          if(indexToInsert === null) {
             // new tail (biggest)
             nodeToInsert.previous = this.tail;
             this.list[this.tail].next = this.list.length;
             this.tail = this.list.length;
          } else if(indexToInsert === this.head) {
             // new head (smallest)
             nodeToInsert.next = this.head;
             this.list[this.head].previous = this.list.length;
             this.head = this.list.length;
          } else {
             nodeToInsert.next = indexToInsert;
             if(this.list[this.list[indexToInsert].previous]?.next != undefined) {
                this.list[this.list[indexToInsert].previous].next = this.list.length;
             }
             nodeToInsert.previous = this.list[indexToInsert].previous;
             this.list[indexToInsert].previous = this.list.length;
          }
          this.list.push(nodeToInsert);
          return 1;
       }
    
       reverse() { // I adapted this to use the (modified) iterator
          for (const node of this.iterator()) {
             [node.next, node.previous] = [node.previous, node.next];
          }
          [this.head, this.tail] = [this.tail, this.head];
       }
       
       sort(sortingFunction) {
          if (!sortingFunction) {return false;}
          
          for (let i = 0; i < this.list.length; i++) {
             let prevNode = null;
             // Iterate in the current order, so by following the links:
             for (let currNode of this.iterator()) { // Requires the change in the generator 
                if (prevNode && sortingFunction(currNode.value, prevNode.value)) {
                   // Get the indexes of the current pair of nodes:
                   let currIndex = prevNode.next;
                   let prevIndex = currNode.previous;
                   // Update links from outer neighbors to these two nodes
                   if (currNode.next != null) this.list[currNode.next].previous = prevIndex;
                   else this.tail = prevIndex;
                   if (prevNode.previous != null) this.list[prevNode.previous].next = currIndex;
                   else this.head = currIndex;
                   // Update links from these two nodes to outer neighbors:
                   currNode.previous = prevNode.previous;
                   prevNode.next = currNode.next;
                   // Update links between the two nodes themselves:
                   prevNode.previous = currIndex;
                   currNode.next = prevIndex;
                } else {
                   prevNode = currNode;
                }
             }
          }
       }
    
       print() { // I adapted this a bit to get more condense output and add a call to printInOrder
          console.log(JSON.stringify(this.list));
          console.log("Head:",this.head,"\nTail:",this.tail, "\nDefault is ascending order.");
          this.printInOrder();
       }
       
       printInOrder() { // I adapted this to use your nice iterator()
          console.log(...Array.from(this.iterator(), node => node.value));
       }
    }
    
    
    const list = new LinkedList();
    
    list.insert(100);
    list.insert(30);
    list.insert(50);
    list.insert(400);
    list.insert(10);
    list.insert(200);
    list.insert(-90);
    
    console.log("When each node is sorted when it is inserted:")
    
    list.print();
    
    list.sort((a, b) => {
      return a > b;
    });
    
    console.log("Now, when re-sorted:");
    
    list.print();

    还有一点需要注意:您的 sortingFunction 返回一个布尔值(指示两个给定参数的顺序是否正确),这与可以传递给本机 Array#sort 方法的比较器函数不同:它是期望返回一个数字,它允许通过返回一个 negative 值、等于 0 和一个 positive 值的反转来指示参数是否处于正确的顺序。您可能希望在实现中遵循相同的“合同”。

    使用更高效的排序算法

    您可以使用 O(nlogn) 合并排序,而不是 O(n²) 冒泡排序。

    我在这里添加了一个mergeSort 方法和一个辅助方法来创建分区:splice。它从链表中提取一个部分(删除它)并将其作为新实例返回。为了使这种拼接工作正常,它使用 same 列表,就像共享内存池一样,但有自己的headtail。这意味着列表的长度不再是链表大小的指示,因此引用length 的一些代码必须更改,例如冒泡排序例程中的外循环:

    class LinkedList {
       constructor(sortingFunction) {
          this.head = null; // Initialise with null explicitly
          this.tail = null;
          this.list = [];
          this.sortingFunction = sortingFunction ?? ((a,b) => {return a < b});
       }
    
       some(func) {  // I adapted this to use the iterator
          for (const node of this.iterator()) {
             if (func(node)) {
                return node.previous == null ? this.head : this.list[node.previous].next;
             }
          }
          return -1;
       }
    
       forEachInOrder(func) { // I adapted this to use the iterator
          for (const node of this.iterator()) func(node);
       }
    
       * iterator() {
          let current = this.head;
          while(current != undefined) {
             const node = this.list[current];
             current = node.next; // <--- move this here!
             yield node;
          }
       }
       
       insert(value) {
          if (this.head == null) { // Avoid using list.length
             this.head = this.list.length; // While here it is appropriate to use list.length!
             this.tail = this.list.length;
             this.list.push({value, next:null,previous:null});
             return 0;
          }
          let nodeToInsert = {value, next:null,previous:null};
          let indexToInsert = this.head;
          let nthnode = this.list[this.head];
          while(nthnode && this.sortingFunction(nthnode.value, value)) {
             indexToInsert = nthnode.next;
             nthnode = this.list[indexToInsert];
          }
          if(indexToInsert === null) {
             // new tail (biggest)
             nodeToInsert.previous = this.tail;
             this.list[this.tail].next = this.list.length;
             this.tail = this.list.length;
          } else if(indexToInsert === this.head) {
             // new head (smallest)
             nodeToInsert.next = this.head;
             this.list[this.head].previous = this.list.length;
             this.head = this.list.length;
          } else {
             nodeToInsert.next = indexToInsert;
             this.list[this.list[indexToInsert].previous].next = this.list.length;
             nodeToInsert.previous = this.list[indexToInsert].previous;
             this.list[indexToInsert].previous = this.list.length;
          }
          this.list.push(nodeToInsert);
          return 1;
       }
    
       reverse() {
          for (const node of this.iterator()) {
             [node.next, node.previous] = [node.previous, node.next];
          }
          [this.head, this.tail] = [this.tail, this.head];
       }
       
       sort(sortingFunction) {
          if (!sortingFunction) return false;
          let dirty = true;
          while (dirty) { // Removed dependency on length
             dirty = false;
             let prevNode = null;
             // Iterate in the current order, so by following the links:
             for (const currNode of this.iterator()) { // Requires the change in the generator 
                if (prevNode && sortingFunction(currNode.value, prevNode.value)) {
                   // Get the indexes of the current pair of nodes:
                   let currIndex = prevNode.next;
                   let prevIndex = currNode.previous;
                   // Update links from outer neighbors to these two nodes
                   if (currNode.next != null) this.list[currNode.next].previous = prevIndex;
                   else this.tail = prevIndex;
                   if (prevNode.previous != null) this.list[prevNode.previous].next = currIndex;
                   else this.head = currIndex;
                   // Update links from these two nodes to outer neighbors:
                   currNode.previous = prevNode.previous;
                   prevNode.next = currNode.next;
                   // Update links between the two nodes themselves:
                   prevNode.previous = currIndex;
                   currNode.next = prevIndex;
                   dirty = true;
                } else {
                   prevNode = currNode;
                }
             }
          }
       }
       
       // Added this method which can also be of use for algorithms that use partitioning
       splice(head, tail) {
          // Remove this slice from the current linked list
          if (tail != this.tail) this.list[this.list[tail].next].previous = this.list[head].previous;
          else this.tail = this.list[head].previous;
          if (head != this.head) this.list[this.list[head].previous].next = this.list[tail].next;
          else this.head = this.list[tail].next;
          this.list[tail].next = null;
          this.list[head].previous = null;
          // Wrap the removed slice into a new linked list instance, but one that shares the memory list
          let slice = new LinkedList(this.sortingFunction);
          slice.list = this.list;
          slice.head = head;
          slice.tail = tail;
          return slice;
       }
    
       mergeSort(sortingFunction) {
          if (!sortingFunction || this.head == null || this.head == this.tail) return; // base case
          // Find last node of first half
          let fastIter = this.iterator();
          fastIter.next();
          let half;
          for (half of this.iterator()) {
             if (fastIter.next().done || fastIter.next().done) break;
          }
          // Split list into two halves
          let right = this.splice(half.next, this.tail);
          let left = this; // ...what remains after the splice.
    
          // Recursively sort the two shorter lists
          left.mergeSort(sortingFunction);
          right.mergeSort(sortingFunction);
          // Make sure the "left" sublist is the one with a head value that comes before the other head value
          if (sortingFunction(this.list[right.head].value, this.list[left.head].value)) [left, right] = [right, left];
          // Merge the two sorted lists
          let tailIndex = left.head;
          let otherIndex = right.head;
          for (let currIndex = this.list[tailIndex].next; currIndex != null || otherIndex != null; currIndex = this.list[tailIndex].next) {
             if (currIndex == null || otherIndex != null && sortingFunction(this.list[otherIndex].value, this.list[currIndex].value)) {
                this.list[tailIndex].next = otherIndex;
                this.list[otherIndex].previous = tailIndex;
                tailIndex = otherIndex;
                otherIndex = currIndex;
             } else {
                tailIndex = currIndex;
             }
          }
          this.head = left.head;
          this.tail = tailIndex;
       }
       
       print() { // I adapted this a bit to get more condense output and add a call to printInOrder
          console.log(JSON.stringify(this.list));
          console.log("Head:",this.head,"\nTail:",this.tail, "\nDefault is ascending order.");
          this.printInOrder();
       }
       
       printInOrder() { // I adapted this to use your nice iterator()
          console.log(...Array.from(this.iterator(), node => node.value));
       }
    }
    
    
    const linked = new LinkedList();
    
    linked.insert(100);
    linked.insert(30);
    linked.insert(50);
    linked.insert(400);
    linked.insert(10);
    linked.insert(200);
    linked.insert(-90);
    
    console.log("When each node is sorted when it is inserted:")
    
    linked.print();
    
    linked.mergeSort((a, b) => {
      return a > b;
    });
    
    console.log("Now, when re-sorted:");
    
    linked.print();

    【讨论】:

    • 谢谢你,这很好用,你比我问的更详细,并且解释得非常好,这就是为什么这是我能想到的最好的答案(我个人有在这个网站上看到)到目前为止。
    • 请注意,我最初只是回答了有关代码中问题的问题。我现在添加了一个在 O(nlogn) 中运行的合并排序替代方案。我没有添加使用原生数组排序的变体,因为 pilchard 已经提供了它。请注意,迭代器在 JS 中并没有那么快,但我喜欢这种编码模式。
    【解决方案2】:

    这是一个利用 Array#sort() 的快速而肮脏的解决方案

    const list = [
      { value: { num: 1, str: 'a' }, next: 3, prev: 2 },
      { value: { num: 2, str: 'e' }, next: 2, prev: null },
      { value: { num: 3, str: 'v' }, next: 0, prev: 1 },
      { value: { num: 4, str: 'g' }, next: null, prev: 0 }];
    let head = 1, tail = 3;
    
    function sort(sortingFunction) {
      const sorted = list
        .map((node, idx) => ({ node, idx }))
        .sort(({ node: { value: a } }, { node: { value: b } }) => sortingFunction(a, b));
    
      for (const [i, { node, idx }] of sorted.entries()) {
        if (!i) {
          node.prev = null;
          head = idx;
        } else node.prev = sorted[i - 1].idx;
    
        if (i === sorted.length - 1) {
          node.next = null;
          tail = idx;
        } else node.next = sorted[i + 1].idx;
    
      }
    }
    
    const log_list = () => { let node = list[head]; while (node !== undefined) { console.log(node.value); node = list[node.next]; } };
    
    // initial order logging
    console.log('initial - ', 'head: ', head, ' tail: ', tail);
    log_list();
    console.log('list order: ', ...list.map((o) => o.value.num));
    
    // sort descending
    sort((a, b) => b.num - a.num);
    
    // after sort logging
    console.log('\nsorted descending - ', 'head: ', head, ' tail: ', tail);
    log_list();
    console.log('list order: ', ...list.map((o) => o.value.num));
    
    // sort alphabetic
    sort((a, b) => a.str.localeCompare(b.str));
    
    // after sort logging
    console.log('\nsorted alphabetic - ', 'head: ', head, ' tail: ', tail);
    log_list();
    console.log('list order: ', ...list.map((o) => o.value.num));
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    以上为您提供了 javascript 引擎 (timsort in V8) 提供的优化排序的优势,但如果您想实现自己的排序,您也可以在列表中排序,而不是对包含它的数组进行排序.

    您需要按值在数组中保持节点有序,这使得这变得更加困难,因为在交换时您需要记住将相邻节点更新为正在比较的节点以保持列表连续。

    这是一个快速冒泡排序,它从头到尾遍历列表,直到没有发生交换。它接受一个回调,该回调应返回 >0,以便在 a 之前对 b 进行排序。

    const list = [
      { value: { num: 1, str: 'a' }, next: 3, prev: 2 },
      { value: { num: 2, str: 'e' }, next: 2, prev: null },
      { value: { num: 3, str: 'v' }, next: 0, prev: 1 },
      { value: { num: 4, str: 'g' }, next: null, prev: 0 }];
    let head = 1, tail = 3;
    
    function sort(sortingFunction) {
      let flag = true, node = list[head];
      while (flag) {
        flag = false;
        while (node.next !== null) {
          if (sortingFunction(node.value, list[node.next].value) > 0) {
            const prev_node = list[node.prev];
            const _node = list[node.next];
            const next_node = list[_node.next];
    
            const temp = node.prev;
    
            if (prev_node) prev_node.next = node.next;
            node.prev = node.next;
            node.next = _node.next;
    
            if (next_node) next_node.prev = _node.prev;
            _node.next = _node.prev;
            _node.prev = temp;
    
            if (_node.prev === null) head = node.prev;
            if (node.next === null) tail = _node.next;
    
            flag = true;
          } else {
            node = list[node.next];
          }
        }
        node = list[head];
      }
    }
    
    const log_list = () => { let node = list[head]; while (node !== undefined) { console.log(node.value); node = list[node.next]; } };
    
    // // initial order logging
    console.log('initial - ', 'head: ', head, ' tail: ', tail);
    log_list();
    console.log('list order: ', ...list.map((o) => o.value.num));
    
    // sort numeric
    sort((a, b) => b.num - a.num);
    
    // after sort logging
    console.log('\nsorted descending - ', 'head: ', head, ' tail: ', tail);
    log_list();
    console.log('list order: ', ...list.map((o) => o.value.num));
    
    // sort str
    sort((a, b) => a.str.localeCompare(b.str));
    
    // after sort logging
    console.log('\nsorted alphabetic - ', 'head: ', head, ' tail: ', tail);
    log_list();
    console.log('list order: ', ...list.map((o) => o.value.num));
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    【讨论】:

    • 第二块效果很好,谢谢。
    • 不用担心,trincots 的解释肯定更彻底,更符合您的代码,即使我们涵盖了相同的基础:)
    • 虽然我只是再次通读了 trincots,并且由于它使用定义长度的嵌套循环实现的方式,它具有 O(n^2) 的固定复杂度(即使列表已经排序)其中,我的第二个示例的最坏情况为 O(n^2),但一旦没有注册交换就会退出。
    • 经过一些测试,您的答案明显更快。 Trincot 的解释非常透彻且很有帮助,但您的回答具有更好的逻辑性。这使得两个答案都一样好,所以我不知道如何处理复选标记。
    • 把它放在 trincot 上没关系,我只是想指出不同之处。
    猜你喜欢
    • 1970-01-01
    • 2018-09-13
    • 1970-01-01
    • 1970-01-01
    • 2016-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多