zoukankan      html  css  js  c++  java
  • 算法原理与实践(链表)

      链表

      大纲
      1. 链表介绍
      2. 基本操作
      3. Dummy Node
      4. 追赶指针技巧
      5. 例题分析

      链表介绍
      单向链表(singly linked list),每个节点有一个 next 指针指向后一个节点,还有一个成员变量用以储存数值;
      双向链表(Doubly Linked List),还有一个 prev 指针指向前一个节点。
      Search: O(n), Del, Add: O(1)

      Remove Duplicates from Sorted List
      Given a sorted linked list, delete all duplicates such that each element appear only once. For example, Given 1->1->2, return 1->2. Given 1->1->2->3->3, return 1->2->3.

    #include <list>
    #include <iostream>
    #include <iterator>
    
    using namespace std;
    
    template<typename T>
    void removeDuplicate(list<T>& l)
    {
        list<T>::iterator chaser = l.begin();
        list<T>::iterator runner = ++l.begin();
    
        while (runner != l.end())
        {
            if (*chaser == *runner)
            {
                runner = l.erase(runner);
            }
            else
            {
                ++chaser;
                ++runner;
            }
        }
    }
    
    template<typename T>
    void dump(list<T> l)
    {
        for (auto &e : l)
        {
            cout << e << " ";
        }
        cout << endl;
    }
    
    int main()
    {
        list<int> l = { 1, 1, 2 };
        removeDuplicate(l);
        dump(l);
    
        list<int> l2 = { 1, 1, 2, 3, 3 };
        removeDuplicate(l2);
        dump(l2);
    }
    View Code

      Remove Duplicates from Sorted List II
      Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.
      For example,
      Given 1->2->3->3->4->4->5, return 1->2->5.
      Given 1->1->1->2->3, return 2->3.

    #include <iostream>
    #include <list>
    #include <unordered_map>
    
    using namespace std;
    
    template<typename T>
    void removeDuplicate2(list<T>& l)
    {
        unordered_map<T, unsigned> m;
    
        for (auto &e : l)
            m[e]++;
    
        list<T>::iterator it = l.begin();
    
        while (it != l.end())
        {
            if (m[*it] > 1)
                it = l.erase(it);
            else
                it++;
        }
    }
    
    template<typename T>
    void dump(const list<T>& l)
    {
        for (auto &e : l)
        {
            cout << e << " ";
        }
        cout << endl;
    }
    
    int main()
    {
        list<int> l = { 1, 2, 3, 3, 4, 4, 5 };
    
        removeDuplicate2(l);
    
        dump(l);
    
    
        list<int> l2 = { 1, 1, 1, 2, 3 };
    
        removeDuplicate2(l2);
    
        dump(l2);
    }
    View Code

      Partition List
      Given a linked list and a value x, write a function to reorder this list such that all nodes less than x come before the nodes greater than or equal to x.
      解题分析:将list分成两部分,但两部分的head节点连是不是null都不确定。但总是可以创建两个dummy节点然后在此基础上append,这样就不用处理边界条件了。

    #include <iostream>
    #include <list>
    
    using namespace std;
    
    template<typename T> 
    list<T>* partitionList(const list<T>& l, const T& val)
    {
        list<T> rightList;
        list<T>* leftList = new list<T>;
    
        for (auto &e : l)
        {
            if (e > val)
                rightList.push_back(e);
            else
                leftList->push_back(e);
        }
    
        leftList->insert(leftList->end(), rightList.cbegin(), rightList.cend());
    
        return leftList;
    }
    
    template<typename T>
    void dump(const list<T>& l)
    {
        for (auto &e : l)
            cout << e;
    
        cout << endl;
    }
    
    int main()
    {
        list<int> l = { 6, 5, 4, 3, 2, 1 };
    
        list<int>* pL = partitionList(l, 3);
    
        dump(*pL);
    
        return 0;
    }
    View Code

      

      追赶指针技巧

      对于寻找list某个特定位置的问题,不妨用两个变量 chaser 与 runner,以不同的速度遍历 list,找到目标位置: ListNode *chaser = head, *runner = head。并且可以用一个简单的小 test case 来验证(例如长度为4和5的list)

      Middle Point

      Find the middle point of linked list.
      解题分析: 寻找特定位置,runner以两倍速前进,chaser 一倍速,当runner到达tail时,chaser即为所求解。

    #include <list>
    #include <iostream>
    
    using namespace std;
    
    template<typename T>
    typename list<T>::iterator findMiddlePoint(list<T> &l)
    {
        list<T>::iterator itRunner = l.begin();
        list<T>::iterator itChaser = l.begin();
    
        while (itRunner != l.end())
        {
            itRunner++;
    
            if (itRunner != l.end())
            {
                itRunner++;
                itChaser++;
            }
        }
    
        return itChaser;
    }
    
    int main()
    {
        list<int> l = { 1, 2, 3, 4, 5 };
    
        cout << *findMiddlePoint(l) << endl;
    
        return 0;
    }
    View Code

      kth to Last element
      Find the kth to last element of a singly linked list
      解题分析:之前类似。只是runner与chaser以相同倍速前进,但runner提前k步出发

    #include <list>
    #include <iostream>
    
    using namespace std;
    
    template<typename T>
    typename list<T>::iterator findKthLast(list<T> &l,const size_t k)
    {
        list<T>::iterator itRunner = l.begin();
        list<T>::iterator itChaser = l.begin();
    
        for (size_t i = 0; i < k; i++)
            itRunner++;
    
        while (itRunner != l.end())
        {
            itRunner++;
            itChaser++;
        }
    
        return itChaser;
    }
    
    int main()
    {
        list<int> l = { 1, 2, 3, 4, 5 };
    
        cout << *findKthLast(l, 1) << endl;
    
        return 0;
    }
    View Code

      

      如何判断一个单链表中有环?
      Given a linked list, determine if it has a cycle in it.

    /*返回nullptr说明没有环,否则返回指向环节点的指针*/
    struct Node* findLoop(struct Node* head) {
        struct Node* runner = head;
        struct Node* chaser = head;
    
        while (runner != nullptr) {
            runner = runner->next;
    
            if (runner->next != nullptr)
                runner = runner->next;
            else
                return nullptr;
    
            if (runner->next == chaser)
                return chaser;
            else
                chaser = chaser->next;
        }
    
        return nullptr;
    }
    View Code

      Circular List Node
      Given a circular linked list, return the node at the beginning of the loop
      解题分析:寻找某个特定位置,用 runner technique。Runner 以两倍速度遍历,假定有 loop,那么 runner 与 chaser 一定能在某点相遇。相遇后,再让 chaser 从 head 出发再次追赶 runner,第二次相遇的节点为 loop 开始的位置。(我对 chaser 第二次再从 head 出发追赶这里不赞同,因为第一次就已经找到了环节点,而且第二次再出发时,找到的还是环节点)。

      代码是只相遇一次,并且相遇的节点就是 loop node。

    #include <iostream>
    
    using namespace std;
    
    const unsigned SIZE = 5;
    typedef int DateType;
    
    struct Node {
        DateType val;
        struct Node *next;
    };
    
    void initList(struct Node **head) {
        struct Node* curr = (*head);
    
        for (int i = 0; i < SIZE; i++) {
            curr->next = new struct Node();
            curr = curr->next;
            curr->val = i;
            curr->next = nullptr;
        }
    }
    
    void print(struct Node* head) {
        unsigned loop = 0;
        struct Node* curr = head;
        while (curr != nullptr) {
            cout << curr->val << " ";
            curr = curr->next;
            if (++loop > SIZE * 2)
                break;
        }
    }
    
    /*返回nullptr说明没有环,否则返回指向环节点的指针*/
    struct Node* findLoop(struct Node* head) {
        struct Node* runner = head;
        struct Node* chaser = head;
    
        while (runner != nullptr) {
            runner = runner->next;
    
            if (runner->next != nullptr)
                runner = runner->next;
            else
                return nullptr;
    
            if (runner->next == chaser)
                return chaser;
            else
                chaser = chaser->next;
        }
    
        return nullptr;
    }
    
    void relesseSingleLine(struct Node **head) {
        struct Node* curr = *head;
        struct Node* runner = *head;
    
        while (curr != nullptr) {
            runner = runner->next;
            delete curr;
            curr = runner;
        }
    }
    
    void releaseLoopLine(struct Node **head) {
        while ( (*head)->next != *head) {
            struct Node *runner = (*head)->next->next;
            delete (*head)->next;
            (*head)->next = runner;
        }
        (*head)->next = nullptr;
    }
    
    void release(struct Node **head) {
        struct Node *loop = findLoop(*head);
        if (loop != nullptr) {
            //has a loop
            releaseLoopLine(&loop);
        }
    
        relesseSingleLine(head);
    }
    
    /*返回第index个位置上的节点指针*/
    struct Node* getPtr(struct Node* head, unsigned index) {
        struct Node* curr = head;
        for (unsigned i = 0; i < index; i++)
            curr = curr->next;
    
        return curr;
    }
    
    int main() {
        struct Node* head = new struct Node();
        initList(&head);
    
        getPtr(head, SIZE)->next = getPtr(head, 3);
    
        cout << findLoop(head)->val << endl;
    
        print(head);
        release(&head);
    
        return 0;
    }
    View Code

      判断两个单链表是否有交点?
      先判断两个链表是否有环,如果一个有环一个没环,肯定不相交;如果两个都没有环,判断两个列表的尾部是否相等;如果两个都有环,判断一个链表上的 Z 点是否在另一个链表上。
      如何找到第一个相交的节点?
      求出两个链表的长度 L1, L2(如果有环,则将 Y 点当做尾节点来算),假设 L1 < L2,用两个指针分别从两个链表的头部开始走,长度为 L2 的链表先走 L2 - L1,然后两个一起走,直到二者相遇。

      Rotate List

      Given a list, rotate the list to the right by k places, where k is non-negative.

      e.g. 

      example k = 4 and list = 10->20->30->40->50->60.
      change to => 50->60->10->20->30->40

    #include <list>
    #include <iostream>
    
    using namespace std;
    
    template<typename T>
    list<T> rotateList(const list<T> &l, const size_t k)
    {
        list<T>::const_iterator itK = l.cbegin();
    
        for (size_t i = 0; i < k; i++)
            itK++;
    
        list<T> l2;
    
        for (list<T>::const_iterator itRight = itK; itRight != l.cend(); itRight++)
            l2.push_back(*itRight);
    
        for (list<T>::const_iterator itLeft = l.cbegin(); itLeft != itK; itLeft++)
            l2.push_back(*itLeft);
    
        return l2;
    }
    
    int main()
    {
        list<int> l1 = { 1, 2, 3, 4, 5 ,6 };
    
        list<int> l2 = rotateList(l1, 4);
    
        for (auto &e : l2)
            cout << e << " ";
    
        return 0;
    }
    View Code

      模式识别
      1.在遍历 Linked list 时,注意每次循环内只处理一个或一对节点。核心的节点只处理当前这一个,否则很容易出现重复处理的问题。

      Reverse Linked List
      Reverse the linked list and return the new head.
      循环遍历linked-list, 每次只处理当前指针的next 变量。
      非递归 vs 递归

    #include <list>
    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    template<typename T>
    list<T> reverseList(list<T> &l)
    {
        list<T> l2;
    
        for (auto &e : l)
            l2.push_front(e);
        
        return l2;
    }
    
    int main()
    {
        list<int> l = { 1, 2, 3, 4, 5, 6, 7, 8 };
    
        list<int> l2 = reverseList(l);
    
        for (auto &e : l2)
            cout << e << " ";
    
        return 0;
    }
    View Code  

      模式识别
      2. Swap Node 问题
      交换两个节点,不存在删除的话,两个节点的prev节点的next指针,以及这两个节点的next指针,会受到影响。总是可以
        a. 先交换两个prev节点的next指针的值;
        b. 再交换这两个节点的next指针的值。
      无论这两个节点的相对位置和绝对位置如何,以上的处理方式总是成立。

      Swap Adjacent(邻近的) Nodes
      Given a linked list, swap every two adjacent nodes and return its head.

    #include <list>
    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    template<typename T>
    void swapAdjancentNode(list<T> &l)
    {
        int tick = 2;
    
        list<T>::iterator itRunner = l.begin();
        list<T>::iterator itChaser = l.begin();
    
        itRunner++;
    
        while (itRunner != l.end())
        {
            if (tick == 2)
            {
                swap(*itRunner, *itChaser);
                tick = 0;
            }
    
            itRunner++;
            itChaser++;
            tick++;
        }
    }
    
    int main()
    {
        list<int> l = { 1, 2, 3, 4, 5, 6, 7};
    
        swapAdjancentNode(l);
    
        for (auto &e : l)
            cout << e << " ";
    
        return 0;
    }
    View Code

      模式识别
      3. 同时处理两个 linked list 的问题,循环的条件一般可以用 while( l1 && l2 ) ,再处理剩下非 NULL 的 list。这样的话,边界情况特殊处理,常规情况常规处理。 

      Add List Sum
      Given two linked lists, each element of the lists is a integer. Write a function to return a new list, which is the “sum” of the given two lists.
        Part a. Given input (7->1->6) + (5->9->2), output 2->1->9.
        Part b. Given input (6->1->7) + (2->9->5), output 9->1->2.
      解题分析:对于 a,靠前节点的解不依赖靠后节点,因此顺序遍历求解即可。对于 b,靠前节点的解依赖于靠后节点(进位),因此必须用递归或栈处理。并且,subproblem 返回的结果,可以是一个自定义的结构(进位 + sub-list)。当然,也可以 reverse List 之后再用 a 的解法求解。

    #include <list>
    #include <iostream>
    
    using namespace std;
    
    list<int> addListSum(const list<int> l1, const list<int> l2)
    {
        list<int> l;
    
        int carry = 0;
    
        list<int>::const_iterator it1 = l1.cbegin();
        list<int>::const_iterator it2 = l2.cbegin();
    
        while (it1 != l1.cend() || it2 != l2.cend())
        {
            int sum = carry + (it1 != l1.cend() ? *it1 : 0) + (it2 != l2.cend() ? *it2 : 0);
    
            carry = sum / 10;
    
            sum = sum % 10;
    
            l.push_back(sum);
    
            if (it1 != l1.cend())
                it1++;
    
            if (it2 != l2.cend())
                it2++;
        }
    
        if (carry)
            l.push_back(carry);
    
        return l;
    }
    
    int main()
    {
        list<int> l1 = { 7, 1, 6 };
        list<int> l2 = { 5, 9, 2, 1 };
    
    
        list<int> l = addListSum(l1, l2);
    
        for (auto e : l)
            cout << e << " ";
    
        return 0;
    }
    View Code

      Merge Two Sorted List
      Merge two sorted linked lists and return it as a new list.

    template<typename T>
    list<T>& merge2SortedList(list<T> &l1, const list<T> &l2)
    {
        list<T>::const_iterator it1 = l1.cbegin();
        list<T>::const_iterator it2 = l2.cbegin();
    
        while (it1 != l1.cend()  &&  it2 != l2.cend())
            (*it1 < *it2) ? it1++ : l1.insert(it1, *it2++);
    
        while(it2 != l2.cend())
            l1.insert(it1, *it2++);
    
        return l1;
    }
    View Code

      

      Merge K Sorted List

      1: ListNode *mergeKLists(vector<ListNode *> &lists) {
      2:   if(lists.size() == 0) return NULL;
      3:   ListNode *p = lists[0];
      4:   for(int i =1; i< lists.size(); i++)
      5:   {
      6:     p = merge2Lists(p, lists[i]);
      7:   }
      8:   return p;
      9: }

    #include <list>
    #include <iostream>
    
    using namespace std;
    
    template<typename T>
    list<T>& merge2SortedList(list<T> &l1, const list<T> &l2)
    {
        list<T>::const_iterator it1 = l1.cbegin();
        list<T>::const_iterator it2 = l2.cbegin();
    
        while (it1 != l1.cend()  &&  it2 != l2.cend())
            (*it1 < *it2) ? it1++ : l1.insert(it1, *it2++);
    
        while(it2 != l2.cend())
            l1.insert(it1, *it2++);
    
        return l1;
    }
    
    template<typename T>
    list<T>* mergeMultiSortedList(list<T> *first, list<T> *last)
    {
        if (!(last - first))
            return nullptr;
    
        for (list<T> *it = first + 1; it != last; it++)
            *first = merge2SortedList(*first, *it);
    
        //*first = merge2SortedList(*first, *last);
    
        return first;
    }
    
    int main()
    {
        list<int> l[4];
        l[0] = { 0, 4, 8};
        l[1] = { 1, 5, 9};
        l[2] = { 2, 6, 10, 14 };
        l[3] = { 3, 7, 11, 15, 19 };
    
        l[0] = *mergeMultiSortedList(l, l + 4);
    
        for (auto e : l[0])
            cout << e << " ";
    
        return 0;
    }
    View Code

      -Better Solution?
      HEAP
        1. Create a heap to store ListNode*, which should sort list nodes in the ascending order or node values.
        2. Insert the first node(head) of each list into the heap, so that we store all the k entries in the heap.
        3. Get the top element in heap and add in to the merged list.
        4. If the top element of heap is the last node in a list, pop it and go to step 3.
        5. If the top element of heap has followers, pop it and push its following node into heap.
        6. Go to step 3 until the heap is empty.

      --代码实现

    #include <iostream>
    #include <list>
    #include <vector>
    
    using namespace std;
    
    template<typename T>
    class Heap
    {
    public:
        Heap() : _v(1) {};
    
        void insert(T val)
        {
            _v.push_back(val);
    
            vector<T>::size_type i;
    
            for (i = _v.size() - 1; _v[i / 2] > val; i /= 2)
            {
                _v[i] = _v[i / 2];
            }
    
            _v[i] = val;
        }
    
        T removeMin()
        {
            if (_v.size() == 1)
            {
                return NULL;
            }
    
            T minElement = _v[1];
            T lastElement = _v[_v.size() - 1];
    
            vector<T>::size_type i, child;
    
            for (i = 1; i * 2 < _v.size(); i = child)
            {
                child = i * 2;
    
                if (child != _v.size() - 1 && _v[child + 1] < _v[child])
                {
                    child++;
                }
    
                if (lastElement > _v[child])
                {
                    _v[i] = _v[child];
                }
                else
                {
                    break;
                }
            }
    
            _v[i] = lastElement;
            _v.pop_back();
    
            return minElement;
        }
    
        unsigned size()
        {
            return _v.size() - 1;
        }
    
    private:
        vector<T> _v;
    };
    
    template<typename T>
    list<T> mergeSortList(vector<list<T>> lists)
    {
        Heap<T> h;
    
        for (auto l : lists)
        {
            for (auto e : l)
            {
                h.insert(e);
            }
        }
    
        list<T> l;
    
        while (h.size() != 0)
        {
            l.push_back(h.removeMin());
        }
    
        return l;
    }
    
    int main()
    {
        vector<list<int>> lists = { {1,5,10}, {2, 6, 11}, {3, 7, 12} };
    
        for (auto e : mergeSortList<int>(lists))
        {
            cout << e << " ";
        }
    
        return 0;
    }
    View Code

      模式识别
      4.如果对靠前节点的处理必须在靠后节点之后,即倒序访问问题,则用 recursion(递归),或者等效地,stack来解决。

      例题

      Traverse the linked list reversely.

    void traverse(ListNode *head) {
        if (head == NULL)
            return;
        traverse(head->next);
        visit(head);
    }

      基础训练
      1. Insert a Node in Sorted List
      2. Remove a Node from Linked List
      3. Reverse a Linked List
      4. Merge Two Linked Lists
      5. Find the Middle of a Linked List

      工具箱
      C++
      Doubly linked list 的实现类是 std::list<T>.
      常用 iterator: begin(), end(), rbegin(), rend().
      常用函数:empty(), size(), push_back(T value), pop_back(T value); erase(iterator pos), insert(iterator pos, T value);
      Java
      Doubly linked list 的实现类是 LinkedList<E>
      常用函数:add(E e), add(int index, E element), remove(int index), addAll(Collection<? Extends E> c), get(int index),

      Reorder List
      Given a singly linked list L: L0→L1→…→Ln-1→Ln, reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→… You must do this in-place without altering the nodes' values.
      Example
      For example, Given 1->2->3->4->null, reorder it to 1->4->2->3->null.

    #include <iostream>
    #include <list>
    #include <algorithm>
    
    using namespace std;
    
    template<typename T>
    void reorderList(list<T> &l)
    {
        list<T>::iterator it1 = ++l.begin();
        list<T>::iterator it2 = --l.end();
    
        for (size_t i = 0; i < l.size() / 2; i++)
        {
            swap(*it1++, *it2--);
        }
    }
    
    int main()
    {
        list<int> l = { 0,1,2,3,4,5,6};
    
        reorderList(l);
    
        for (auto &e : l)
        {
            cout << e << " ";
        }
    
        return 0;
    }
    View Code

      Clone a linked list with next and random pointer
      A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null. Return a deep copy of the list.

      solution 1: using hashmap

      batter solution ?

      Homework

      Remove Duplicates from Unsorted List
      Write a removeDuplicates() function which takes a list and deletes any duplicate nodes from the list. The list is not sorted.
      For example
      if the linked list is 12->11->12->21->41->43->21, then removeDuplicates() should convert the list to 12->11->21->41->43. If temporary buffer is not allowed, how to solve it?

    #include <unordered_map>
    #include <list>
    #include <iostream>
    
    using namespace std;
    
    template<typename T>
    list<T> removeDuplicateFromUnsortedList(const list<T> &l)
    {
        unordered_map<T, bool> m;
        list<T> l2;
    
        for (auto &e : l)
        {
            if (m.find(e) == m.end())
            {
                m[e] = true;
                l2.push_back(e);
            }
        }
    
        return l2;
    }
    
    int main()
    {
        list<int> l = { 12,11,12,21,41,43,21 };
    
        list<int> l2 = removeDuplicateFromUnsortedList(l);
    
        for (auto &e : l2)
            cout << e << " ";
    
        return 0;
    }
    View Code

      Reverse a linked list
      Reverse a linked list from position m to n.
      Note
      Given m, n satisfy the following condition: 1 ≤ m ≤ n ≤ length of list.
      Example
      Given 1->2->3->4->5->NULL, m = 2 and n = 4, return 1->4->3->2->5->NULL.
      Challenge
      Reverse it in-place and in one-pass

    #include <iostream>
    #include <list>
    #include <algorithm>
    
    using namespace std;
    
    template<typename T>
    void reverseLinkedList(list<T> &l, size_t n, size_t m)
    {
        list<T>::iterator itN = l.begin();
        list<T>::iterator itM = l.begin();
    
        for (size_t i = 1; i < n; i++)
            itN++;
    
        for (size_t i = 1; i < m; i++)
            itM++;
    
        while (n < m)
        {
            swap(*itN++, *itM--);
            n++; m--;
        }
    }
    
    int main()
    {
        list<int> l = { 1,2,3,4,5, 6 };
    
        reverseLinkedList(l, 2, 5);
    
        for (auto &e : l)
            cout << e << " ";
    
        return 0;
    }
    View Code

      Palindrome List
      Given a singly linked list of characters, write a function that returns true if the given list is palindrome, else false.

    #include <iostream>
    #include <list>
    #include <string>
    
    using namespace std;
    
    template<typename T>
    bool palindromeList(const list<T>& l)
    {
        list<T>::const_iterator it1 = l.cbegin();
        list<T>::const_reverse_iterator it2 = l.crbegin();
    
        for (list<T>::size_type i = 1; i < l.size() / 2; i++)
        {
            it1++; it2++;
        }
    
        while(it1 != l.cend()  &&  it2 != l.crend())
        {
            if (*it1++ != *it2++)
                return false;
        }
    
        return true;
    }
    
    int main()
    {
        string str = "aabbac";
        list<char> l;
    
        for (auto &e : str)
            l.push_back(e);
    
        if (palindromeList(l))
            cout << "Yes" << endl;
    
        return 0;
    }
    View Code

      

    <全文完>

  • 相关阅读:
    已解决: 已引发: "无法加载 DLL“opencv_core2410”: 找不到指定的模块。
    Xcode 设置图片全屏显示
    独创轻松实现拖拽,改变层布局
    WCF Odata 开放数据协议应用
    MVC中,加入的一个aspx页面用到AspNetPager控件处理办法
    关于 HRESULT:0x80070
    Springboot文件上传大小设置
    Jquery Validate 表单验证使用
    Quartz任务调度框架使用
    js中常见命令
  • 原文地址:https://www.cnblogs.com/fengyubo/p/5111548.html
Copyright © 2011-2022 走看看