zoukankan      html  css  js  c++  java
  • 链表

    链表

      链表是采用链式结构存储的线性表。链表中的元素在存储空间中的位置不一定是连续的,所以链表使用结点来存储元素,每个节点中还存储了相邻节点位置信息。由于不是连续存储,存取元素的速度比顺序表差。但是只要存储空间足够,链表就可以动态增加长度,也就是说,相较于顺序表,链表能更快速地进行元素的插入和删除操作。

      链表需要一个头指针head来表示链表的第一个结点。根据结点中存储的相邻结点信息的不同,链表又可以细分为单向链表和双向链表。若链表的第一个结点和最后一个结点相连,则该链表又可以称为循环链表。

    单向链表

      单向链表的结点中只保存了直接后继结点的位置信息。也就是说,每一个结点中都有一个next指针指向该结点的直接后继结点,若该结点是最后一个结点,则next取null。

      

      单向链表的结点结构定义如下:

     1 public class LNode<E> {
     2 
     3     public E data;
     4 
     5     public LNode<E> next;
     6 
     7     public LNode(E data) {
     8         this.data = data;
     9     }
    10 
    11 }
    LNode

    查找

      单向链表通过遍历来找到指定位标的结点:从head指向的结点(第一个结点)开始遍历,遍历到next取值为null的结点(最后一个结点)时结束。

     1 public LNode<E> getNode(int index) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     if (isEmpty()) throw new ListException("链表为空!");
     4     // 从head指针指向的结点开始
     5     LNode<E> node = head;
     6     // index == 0表示当前结点为待查询结点,node.next == null表示当前结点为最后一个结点
     7     while (index > 0 && node.next != null) {
     8         node = node.next;
     9         index--;
    10     }
    11     return node;
    12 }
    getNode

      可以根据指定元素来查找结点是否存在。

     1 public LNode<E> getNodeByElem(E e) {
     2     if (isEmpty()) throw new ListException("链表为空!");
     3     // 从head指针指向的结点开始
     4     LNode<E> node = head;
     5     // node == null表示查找不到指定的结点
     6     while (node != null) {
     7         if (e.equals(node.data)) break;
     8         node = node.next;
     9     }
    10     return node;
    11 }
    getNodeByElem

    插入

      单向链表的插入分为三种情况:

      在前面插入结点:插入结点的next指针指向第一个结点,head指针指向插入结点。

      

      在后面插入结点:最后一个结点的next指针指向插入结点。

      

      在中间插入结点:指定位置的前一个结点的next指针指向插入结点,插入结点的next指针指向指定位置的结点。

      

     1 public void add(int index, E data) {
     2     LNode<E> node = new LNode<E>(data);
     3     // head == null表示链表为空,index == 0表示在前面插入结点
     4     if (head == null || index == 0) {
     5         node.next = head;
     6         head = node;
     7     } else {
     8         // 获取指定位置的前一个结点
     9         LNode<E> n = getNode(index - 1);
    10         // n.next == null表示在后面插入结点,n.next != null表示在中间插入结点
    11         node.next = n.next;
    12         n.next = node;
    13     }
    14 }
    add

    删除

      单向链表的删除也分为三种情况:

      删除第一个结点:head指针指向下一个结点。

      

      删除最后一个结点:倒数第二个结点的next指针置为null。

      

      删除中间的结点:指定结点前一个结点的next指针指向指定结点下一个结点。

      

     1 public E remove(int index) {
     2     // head != null表示链表不为空
     3     if (head != null) {
     4         E e;
     5         // index == 0表示删除第一个结点
     6         if (index == 0) {
     7             e = head.data;
     8             head = head.next;
     9         } else {
    10             LNode<E> node = head;
    11             // index == 1表示当前结点是指定结点的前一个结点,node.next.next == null表示当前结点是倒数第二个结点
    12             while (index > 1 && node.next.next != null) {
    13                 node = node.next;
    14                 index--;
    15             }
    16             e = node.next.data;
    17             node.next = node.next.next;
    18         }
    19         return e;
    20     }
    21     return null;
    22 }
    remove

    反转

      实现单向链表的反转可以通过将第一个结点与之后的结点断开,之后每一个结点在前面插入,最后head指针指向最后一个结点。

     1 public void reverse() {
     2     // head == null表示链表为空,head.next == null表示链表中只有一个结点
     3     if (head != null && head.next != null) {
     4         LNode<E> node = head.next;
     5         head.next = null;
     6         while (node != null) {
     7             LNode<E> n = node.next;
     8             // 在前面插入当前结点
     9             node.next = head;
    10             head = node;
    11             // 走向下一个结点
    12             node = n;
    13         }
    14     }
    15 }
    reverse

    单向循环链表

      单向循环链表是第一个结点和最后一个结点相连的单向链表。单向循环链表最后一个结点的next指针指向第一个结点。

      

      在单向循环链表中可以添加一个size变量来存储结点个数。

    查找

      单向循环链表可以通过循环查找指定位标的结点。指定位标可以通过对size求模来减少循环次数。

     1 public LNode<E> getNode(int index) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     if (isEmpty()) throw new ListException("链表为空!");
     4     index %= size;
     5     LNode<E> node = head;
     6     while (index > 0) {
     7         node = node.next;
     8         index--;
     9     }
    10     return node;
    11 }
    getNode

      可以通过指定元素来查找结点是否存在。

    1 public LNode<E> getNodeByElem(E e) {
    2     if (isEmpty()) throw new ListException("链表为空!");
    3     LNode<E> node = head;
    4     do {
    5         if (e.equals(node.data)) return node;
    6         node = node.next;
    7     } while (node != head);   // node == head表示node已经走完一圈
    8     return null;
    9 }
    getNodeByElem

    插入

      单向循环链表的插入分为两种:

      在前面插入结点:插入结点的next指针指向第一个结点,最后一个结点的next指针指向插入结点,head指针指向插入结点。

      

      在中间插入结点:插入结点的next指针指向指定位置的结点,指定位置的前一个结点的next指针指向插入结点。

      

     1 public void add(int index, E data) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     LNode<E> node = new LNode<E>(data);
     4     if (isEmpty()) {
     5         // 链表为空表,则创建一个自成环形的结点
     6         node.next = node;
     7         head = node;
     8     } else {
     9         // 对size求模减少循环次数
    10         index %= size;
    11         LNode<E> n;
    12         if (index == 0) {
    13             // 在前面插入结点,获取的是最后一个结点
    14             n = getNode(size - 1);
    15             head = node;
    16         } else {
    17             // 在中间插入结点,获取的是前一个结点
    18             n = getNode(index - 1);
    19         }
    20         node.next = n.next;
    21         n.next = node;
    22     }
    23     size++;
    24 }
    add

    删除

      单向循环链表的删除也分为两种:

      删除第一个结点:最后一个结点的next指针指向第二个结点,head指针指向下一个结点。

      

      删除其他结点:指定结点前一个结点的next指针指向指定结点下一个结点。

      

     1 public E remove(int index) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     if (isEmpty()) return null;
     4     E e = null;
     5     if (size == 1) {
     6         e = head.data;
     7         head = null;
     8     } else {
     9         index %= size;
    10         LNode<E> node;
    11         if (index == 0) {
    12             // 获取最后一个结点
    13             node = getNode(size - 1);
    14             head = head.next;
    15         } else {
    16             // 获取前一个结点
    17             node = getNode(index - 1);
    18         }
    19         e = node.next.data;
    20         node.next = node.next.next;
    21     }
    22     size--;
    23     return e;
    24 }
    remove

    反转

      单向循环链表的反转与单向链表类似,不过需要先定义一个辅助结点保存反转后的最后一个结点,最后一步需要让最后一个结点的next指针指向反转后的第一个结点(head指针指向的结点)。

     1 public void reverse() {
     2     // size == 0表示链表为空,size == 1表示链表只有一个结点
     3     if (size <= 1) return;
     4     // 反转后的最后一个结点为现在的第一个结点
     5     LNode<E> tail = head;
     6     // 从第二个结点开始遍历
     7     LNode<E> node = head.next;
     8     while (node != tail) {
     9         LNode<E> n = node.next;
    10         // 在前面插入结点
    11         node.next = head;
    12         head = node;
    13         // 走向下一个结点
    14         node = n;
    15     }
    16     // 最后一个结点的next指针指向第一个结点
    17     tail.next = head;
    18 }
    reverse

    双向链表

      双向链表的结点中保存了直接前驱结点和直接后继结点的位置信息。也就是说,每一个结点中都有一个next指针指向直接后继结点,一个prior指针指向直接前驱结点。

      

      双向链表的结点结构定义如下:

     1 public class DuLNode<E> {
     2 
     3     public E data;
     4 
     5     public DuLNode<E> prior;
     6 
     7     public DuLNode<E> next;
     8 
     9     public DuLNode(E data) {
    10         this.data = data;
    11     }
    12 
    13 }
    DuLNode

    查找

      双向链表也是通过遍历找到指定位标的结点。

     1 public DuLNode<E> getNode(int index) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     if (isEmpty()) throw new ListException("双向链表为空!");
     4     DuLNode<E> node = head;
     5     while (index > 0 && node.next != null) {
     6         node = node.next;
     7         index--;
     8     }
     9     return node;
    10 }
    getNode

      可以通过指定元素来查找结点是否存在。

    1 public DuLNode<E> getNodeByElem(E e) {
    2     if (isEmpty()) throw new ListException("双向链表为空!");
    3     DuLNode<E> node = head;
    4     while (node != null) {
    5         if (e.equals(node.data)) return node;
    6         node = node.next;
    7     }
    8     return null;
    9 }
    getNodeByElem

    插入

      双向链表的插入与单向链表一样,只不过在插入结点时除了设置next指针外,还要设置prior指针。

     1 public void add(int index, E data) {
     2     DuLNode<E> node = new DuLNode<E>(data);
     3     if (index < 0) throw new ListException("位标不能为负!");
     4     if (head == null) {
     5         head = node;
     6     } else {
     7         // index == 0表示在前面插入结点
     8         if (index == 0) {
     9             node.next = head;
    10             head.prior = node;
    11             head = node;
    12         } else {
    13             DuLNode<E> n = getNode(index - 1);
    14             node.next = n.next;
    15             // n.next == null表示在后面插入结点,n.next != null表示在中间插入结点
    16             if (n.next != null) n.next.prior = node;
    17             n.next = node;
    18             node.prior = n;
    19         }
    20     }
    21 }
    add

    删除

      双向链表的删除也是在单向链表的基础上多一步对prior指针的设置。

     1 public E remove(int index) {
     2     if (isEmpty()) return null;
     3     E e = null;
     4     if (head.next == null) {
     5         e = head.data;
     6         head = null;
     7     } else {
     8         if (index == 0) {
     9             e = head.data;
    10             head.next.prior = null;
    11             head = head.next;
    12         } else {
    13             DuLNode<E> node = getNode(index - 1);
    14             if (node.next != null) {
    15                 node = node.next;
    16                 e = node.data;
    17                 node.next.prior = node.prior;
    18                 node.prior.next = node.next;
    19             }
    20         }
    21     }
    22     return e;
    23 }
    remove

    双向循环链表

      双向循环链表是第一个结点和最后一个结点相连的双向链表。双向循环链表最后一个结点的next指针指向第一个结点,第一个结点的prior指针指向最后一个结点。

      

      在双向循环链表中可以添加一个size变量来存储结点个数。

    查找

      双向循环链表可以通过循环查找指定位标的结点。指定位标可以通过对size求模来减少循环次数。

     1 public DuLNode<E> getNode(int index) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     if (isEmpty()) throw new ListException("链表为空!");
     4     index %= size;
     5     DuLNode<E> node = head;
     6     while (index > 0) {
     7         node = node.next;
     8         index--;
     9     }
    10     return node;
    11 }
    getNode

      可以通过指定元素来查找结点是否存在。

    1 public DuLNode<E> getNodeByElem(E e) {
    2     if (isEmpty()) throw new ListException("链表为空!");
    3     DuLNode<E> node = head;
    4     do {
    5         if (e.equals(node.data)) return node;
    6         node = node.next;
    7     } while (node != head);
    8     return null;
    9 }
    getNodeByElem

    插入

      双向循环链表的插入是在单向循环链表的基础上多一步对prior指针的设置。

     1 public void add(int index, E data) {
     2     DuLNode<E> node = new DuLNode<E>(data);
     3     if (head == null) {
     4         node.prior = node.next = node;
     5         head = node;
     6     } else {
     7         index %= size;
     8         if (index == 0) {
     9             node.prior = head.prior;
    10             node.next = head;
    11             head.prior.next = node;
    12             head.prior = node;
    13             head = node;
    14         } else {
    15             DuLNode<E> n = getNode(index - 1);
    16             node.prior = n;
    17             node.next = n.next;
    18             n.next.prior = node;
    19             n.next = node;
    20         }
    21     }
    22     size++;
    23 }
    add

    删除

      双向循环链表的删除是在单向循环链表的基础上多一步对prior指针的设置。

     1 public E remove(int index) {
     2     if (index < 0) throw new ListException("位标不能为负!");
     3     if (isEmpty()) return null;
     4     E e = null;
     5     if (size == 1) {
     6         e = head.data;
     7         head = null;
     8     } else {
     9         index %= size;
    10         if (index == 0) {
    11             e = head.data;
    12             head.next.prior = head.prior;
    13             head.prior.next = head.next;
    14             head = head.next;
    15         } else {
    16             DuLNode<E> node = getNode(index);
    17             e = node.data;
    18             node.next.prior = node.prior;
    19             node.prior.next = node.next;
    20         }
    21     }
    22     size--;
    23     return e;
    24 }
    remove

    约瑟夫问题

      据说著名犹太历史学家约瑟夫有过这样一个故事:在罗马人占领乔塔帕特后,39个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式:41个人排成一个圆圈,由第1个人开始报数,每报到3的人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从,于是约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,最终逃过了这场死亡游戏。

      这个故事演化来的问题就是著名的约瑟夫问题:由n个人围成一个圈,从1开始报数,报到m的人就出圈,由下一个人继续从1开始报数......求出出圈的人的顺序。

      例如编号分别为1~5的5个小朋友围成一个圈,从1号小朋友开始报数,每次报到2的小朋友出圈:

      a. 1号报1,2号报2,所以2号出圈。此时圈内剩下1-3-4-5。

      b. 3号报1,4号报2,所以4号出圈。此时圈内剩下1-3-5。

      c. 5号报1,1号报2,所以1号出圈。此时圈内剩下3-5。

      d. 3号报1,5号报2,所以5号出圈。此时圈内剩下3。

      综上所述,出圈的顺序为2-4-1-5-3。

    解决方案

      约瑟夫问题的特点是:

      1. 所有人围成一个圈。也就是说,最后一个人的下一个人是第一个人。所以,可以使用单向环形链表来表示。

      2. 从当前的人开始报数,报到m的人出圈。也就是说,出圈的人是当前的人之后的第m - 1个人。所以,如果假设当前人的编号为i,那么下一个出圈的人就是(i + m - 1) % size(i + m - 1可能大于size,所以需要通过对size求模),即删除(i + m - 1) % size结点。

      所以,求解约瑟夫问题方法的定义如下:

     1 /**
     2  * 求解约瑟夫问题
     3  * @param m  报到m的出圈
     4  * @return  按出圈的顺序排列的数组
     5  */
     6 @SuppressWarnings("unchecked")
     7 public static <E> E[] josephus(E[] datas, int m) {
     8     if (datas == null || datas.length == 0) throw new ListException("没有数据!");
     9     E[] result = (E[]) Array.newInstance(datas[0].getClass(), datas.length);
    10     CirLinkedList<E> l = new CirLinkedList<E>();
    11     l.addAll(datas);
    12     int i = 0;   // 从第一个人开始报数
    13     int j = 0;
    14     while (! l.isEmpty()) {
    15         i = (i + m - 1) % l.size();
    16         result[j++] = l.remove(i);
    17     }
    18     return result;
    19 }
    josephus

      对该方法进行测试,输入人数n=5,报数m=2,输出结果为:

      

      可以看到该结果与原先分析得出的结果一致。

      输入人数n=41,报数m=3,输出结果为:

      

      可以看到,结果的最后两个编号为16和31。这就是约瑟夫将朋友与自己安排在第16个与第31个位置,最终逃过了死亡游戏的原因。

  • 相关阅读:
    python3.x 基础五:模块
    python3.x 基础四:目录获取及目录规范
    python3.x 基础四:json与pickple
    python3.x 基础四:生成器与迭代器
    python3.x 基础三:装饰器
    python3.x 基础三:函数
    [leetcode]Anagrams
    [leetcode]Text Justification
    [leetcode]Single Number
    [leetcode]Longest Palindromic Substring
  • 原文地址:https://www.cnblogs.com/lqkStudy/p/11518006.html
Copyright © 2011-2022 走看看