• 【数据结构Java版】LinkedList与链表


    目录

    一、什么是LinkedList

    二、LinkedList的模拟实现

    (1)合法LinkedList的要求

    (2)LinkedList实现的内容

    1.结点个数

    2.链表尾插元素

    3.链表头插元素

    4.指定下标插入元素

    5.删除指定下标结点

    6.删除指定元素

    7.获取指定下标元素

    8.设置指定下标元素

    9.从前往后获取指定元素下标

    10.从后往前获取指定元素下标

    11.判断是否包含指定元素

    12.清空链表

    13.判断链表是否为空

    三、LinkedList的使用

    (1)LinkedList的构造

    (2)LinkedList的其他常用方法介绍

    (3)LinkedList的遍历

    四、ArrayList与LinkedList的比较


    一、什么是LinkedList

            LinkedList 的官方文档:        LinkedList (Java Platform SE 8 )
            单链表从前往后遍历支持较好,从后往前遍历支持较差。 因此LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的结点中,然后通过引用将结点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

            不太清楚单链表知识的小朋友建议先看这:http://t.csdn.cn/c8WZ2

    LinkedList示意图:

    模拟实现LinkedList的属性 :      

    1. public class MyLinkedList implements MyList {
    2. // 维护着 3 个属性
    3. // 1. 链表的头结点
    4. private MyNode head;
    5. // 2. 链表的尾结点
    6. private MyNode last;
    7. // 3. 维护着链表中的元素个数
    8. private int size; // 不维护也可以通过遍历数出来,但维护好这个属性,可以让 size 的时间复杂度 O(n) -> O(1)
    9. // 构造方法
    10. // 构造一个空的链表
    11. public MyLinkedList() {
    12. this.head = this.last = null;
    13. this.size = 0;
    14. }

            在集合框架中,LinkedList也实现了List接口,具体如下:

    1. LinkedList实现了List接口
    2. LinkedList的底层使用了双向链表
    3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
    比特就业课
    4. LinkedList的任意位置插入和删除元素时效率比较,时间复杂度为O(1)
    5. LinkedList比较适合任意位置插入的场景

     


    二、LinkedList的模拟实现

    (1)合法LinkedList的要求

            一个正确的 MyLinkedList 应该做到什么 ?要始终在模拟实现的过程关注以下几点内容,判断模拟的链表是否是合法的。

            为了在实现LinkedList模拟过程时刻判断链表是否合法,我们可以设置一套检查装置,这套检查代码不用掌握。我们自己编写代码是要注意时刻检查调试代码是否正确,不要一股脑儿的写代码,最后bug满天飞,无法入手。

    1. 得是一个合法的线性表

    2. 当 head != null 则 last != null 反之亦然 当 head == null 则 last == null

    3. 当 head != null 的时候,则 size > 0 同理,当 head == null 的时候,则 size == 0

    4. size 的值应该 == 通过遍历数出来的结点个数 (从 head 到 last 或者 从 last 到 head)

    5. 除了 head 和 last 之外,所有的其他结点(node), node.prev != null && node.next != null

    6. head != null 时,则 head.prev == null,但 head.next 不确定(可以不为 null | size > 1);也可      以为 null | size == 1) last != null 时,则 last.next == null,但 last.prev 不确定(可以不为 null |        size > 1);也可以为 null | size == 1)

    7. 除了 head 和 last 之外的所有结点(node),node.prev.next == node && node.next.prev == node 8. 当 head.next != null 时,head.next.prev == head 当 last.prev != null 时,last.prev.next == last 9. 当 size == 1 时,head == last && head != null

    1. private static void 断言为真(boolean condition, String message) {
    2. if (!condition) {
    3. throw new RuntimeException(message);
    4. }
    5. }
    6. private static void 检查2(MyLinkedList list) {
    7. if (list.head == null) {
    8. 断言为真(list.last == null, "head 为 null 时,last 必须是 null");
    9. } else {
    10. 断言为真(list.last != null, "head 不为 null 时,last 必须不为 null");
    11. }
    12. }
    13. private static void 检查3(MyLinkedList list) {
    14. 断言为真(list.size >= 0, "size 必须 >= 0");
    15. if (list.head == null) {
    16. 断言为真(list.size == 0, "head 为 null 时,size 必须是 0");
    17. } else {
    18. 断言为真(list.size > 0, "head 不为 null 时,size 必须大于 0");
    19. }
    20. }
    21. private static int 从前往后遍历确定结点个数(MyLinkedList list) {
    22. int size = 0;
    23. for (MyNode cur = list.head; cur != null; cur = cur.next) {
    24. size++;
    25. }
    26. return size;
    27. }
    28. private static void 检查4(MyLinkedList list) {
    29. 断言为真(list.size == 从前往后遍历确定结点个数(list), "记录的 size 应该和遍历出的 size 相等");
    30. }
    31. private static void 检查5(MyLinkedList list) {
    32. if (list.size > 1) {
    33. MyNode cur = list.head.next;
    34. while (cur != list.last) {
    35. 断言为真(cur.prev != null, "非头尾结点的 prev 不能是 null");
    36. 断言为真(cur.next != null, "非头尾结点的 next 不能是 null");
    37. cur = cur.next;
    38. }
    39. }
    40. }
    41. private static void 检查6(MyLinkedList list) {
    42. // head != null 等价于 size > 0
    43. if (list.head != null) {
    44. 断言为真(list.head.prev == null, "头结点的 prev 一定是 null");
    45. 断言为真(list.last.next == null, "尾结点的 next 一定是 null");
    46. if (list.size == 1) {
    47. 断言为真(list.head.next == null, "size 为 1 时,头节点的 next 一定是 null");
    48. 断言为真(list.last.prev == null, "size 为 1 时,为节点的 prev 一定是 null");
    49. } else {
    50. 断言为真(list.head.next != null, "size > 1 时,头节点的 next 一定不是 null");
    51. 断言为真(list.last.prev != null, "size > 1 时,尾节点的 prev 一定不是 null");
    52. }
    53. }
    54. }
    55. private static void 检查7(MyLinkedList list) {
    56. if (list.size > 1) {
    57. MyNode cur = list.head.next; // 跳过头节点
    58. while (cur != list.last) { // 跳过尾结点
    59. 断言为真(cur.prev.next == cur, "非头尾结点的 前驱的 后继是自己");
    60. 断言为真(cur.next.prev == cur, "非头尾结点的 后继的 前驱是自己");
    61. cur = cur.next;
    62. }
    63. }
    64. }
    65. private static void 检查8(MyLinkedList list) {
    66. // head.next != null 等价于 last.prev != null 等价于 size > 1
    67. if (list.size > 1) {
    68. 断言为真(list.head.next.prev == list.head, "当 head.next != null 时,head.next.prev == head");
    69. 断言为真(list.last.prev.next == list.last, "当 last.prev != null 时,last.prev.next == last");
    70. }
    71. }
    72. private static void 断言是一个合法的链表(MyLinkedList list) {
    73. 检查2(list);
    74. 检查3(list);
    75. 检查4(list);
    76. 检查5(list);
    77. 检查6(list);
    78. 检查7(list);
    79. 检查8(list);
    80. }

    (2)LinkedList实现的内容

    //结点个数
        public int size() {
            return 0;
        }

    //链表尾插元素
        public boolean add(Long e) {
            return false;
        }

    //链表头插元素

         public boolean addFirst(Long e) {

            return false;

        }

    //指定下标插入元素
        public void add(int index, Long e) {

        }

    //删除指定下标结点
        public Long remove(int index) {
            return null;
        }

    //删除指定元素
        public boolean remove(Long e) {
            return false;
        }

    //获取指定下标元素
        public Long get(int index) {
            return null;
        }

    //设置指定下标元素
        public Long set(int index, Long e) {
            return null;
        }

    //从前往后获取指定元素下标
        public int indexOf(Long e) {
            return 0;
        }

    //从后往前获取指定元素下标
        public int lastIndexOf(Long e) {
            return 0;
        }

    //判断是否包含指定元素
        public boolean contains(Long e) {
            return false;
        }

    //清空链表
        public void clear() {

        }

    //判断链表是否为空
        public boolean isEmpty() {
            return false;
        }


    1.结点个数

            在检查机制中已经写过一遍,就是从前往后遍历所有元素。这里主要要返回size即可

    1. public int size() {
    2. return size
    3. }

    2.链表尾插元素

    时间复杂度是O(1)

    考虑两种情况:size==0的情况和链表size>0的情况。

    注意要点 :

    a.需要分情况讨论。两种情况无法合并

    b.既要关注head,也要关注last

    c.需要正确处理结点的next和prev。涉及新加入的结点和之前的尾结点(如果存在的话)

    d.size不能忘记

     

    1. public boolean add(Long e){
    2. //1.将元素装入新结点
    3. MyNode node=new MyNode(e);
    4. node.next=null;//这一步可以省略
    5. //2.找尾结点,分情况讨论
    6. //2.1链表中有尾结点:last != null 等价于 head != null 等价于 size > 0
    7. if(size>0){
    8. this.last.next=node;
    9. node.prev=this.last;
    10. this.last=node;
    11. }else{
    12. //2.2链表中没有有尾结点
    13. node.prev=null;
    14. this.head=this.last=node;
    15. }
    16. //3.size个数增加
    17. this.size++;
    18. return true;
    19. }

    3.链表头插元素

     时间复杂度是O(1)

    与尾插思路基本一致。

     

    1. public boolean addFisrst(Long e){
    2. MyNode node=new MyNode(e);
    3. node.prev=null;
    4. if(size>0){
    5. this.head.prev=node;
    6. node.next=this.head;
    7. this.head=node;
    8. }else{
    9. node.next=null;
    10. this.head=this.last=node;
    11. }
    12. this.size++;
    13. return true;
    14. }

    4.指定下标插入元素

            时间复杂度是O(n)

            根据下标的特征可以分为以下几种情况。根据以下表格写代码,才可以保证所有情况都考虑到不出错。

     

    1. public void add(int index,int e){
    2. if(index<0||index>size){
    3. throw new ArrayIndexOfBoundsException("下标不合法");
    4. }
    5. //情况1 size==0
    6. if(size==0){
    7. add(e);//尾插,或者addFirst(e)用头插也可以
    8. return;
    9. }
    10. //情况2 size==1
    11. if(size==1){
    12. //2.1 index==0
    13. if(index==0){
    14. addFirst(e);
    15. //2.2 index==1
    16. }else{
    17. add(e);
    18. }
    19. return;
    20. }
    21. //情况3 size>1
    22. //3.1 index==0
    23. if(index==0){
    24. addFirst(e);
    25. return;
    26. //3.2 index=size
    27. }else if(index==size){
    28. add(e);
    29. return;
    30. }
    31. //3.3 0
    32. // 链表要插入,是要找到前驱结点
    33. // 由于我们是双向链表,前驱结点很容易找
    34. // 先找到前驱结点,记为 prevNode
    35. MyNode prevNode = this.head;
    36. //找到指定下标的前一个位置
    37. for(int i=0;i1;i++){
    38. prevNode=prevNode.next;
    39. }
    40. //找到指定下标的位置
    41. MyNode curNode=prevNode.next;
    42. //将元素e装入结点
    43. MyNode node=new Mynode(e);
    44. //将该结点插入指定下标位置处
    45. prevNode.next=node;
    46. curNode.prev=node;
    47. node.prev=prevNode;
    48. node.next=curNode;
    49. //size的值增加
    50. size++;
    51. }

     关于异常操作,指路☞  http://t.csdn.cn/QaTL8


    5.删除指定下标结点

            时间复杂度是O(n)

            注意size==0的情况是下标不合法。


     

     

    1. public Long remove(int index){
    2. //下标不合法
    3. if(index<0||index>=size){
    4. throw new ArrayIndexOutOfBoundsException("下标不合法");
    5. }
    6. //下标合法
    7. //情况1:size==1
    8. if(size==1){
    9. //现将要删除的结点元素保存下来
    10. Long e=this.head.val;
    11. this.head=this.last=null;
    12. //size减少
    13. this.size==0;
    14. return e;
    15. }
    16. //情况2:size>1
    17. //2.1 index==0 头删
    18. if(index==0){
    19. Long e=this.head.val;
    20. //改变头结点的位置,现头结点为原来头结点的后一个
    21. this.head=this.head.next;
    22. //现头结点的前驱指向为null
    23. this.head.prev=null;
    24. //size减少
    25. this.size--;
    26. return e;
    27. }
    28. //2.2 index==size-1 尾删
    29. if(index==size-1){
    30. Long e=this.last.val;
    31. //改变尾结点的位置,现尾结点为原来尾结点的前一个
    32. this.last=this.last.prev;
    33. //现尾结点的前驱指向为null
    34. this.last.next=null;
    35. //size减少
    36. this.size--;
    37. return e;
    38. }
    39. //size>1 && 0
    40. //找到指定下标结点
    41. MyNode curNode=this.head;
    42. for(int i=0;i
    43. curNode=curNode.next;
    44. }
    45. Long e=curNode.val;
    46. //指定下标结点的前一个结点
    47. MyNode prevNode=curNode.prev;
    48. //指定下标结点的后一个结点
    49. MyNode nextNode=curNode.next;
    50. //删除操作
    51. preNode.next=nextNode;
    52. nextNode.prev=prevNode;
    53. this.size--;
    54. return e;
    55. }

      关于异常操作,指路☞  http://t.csdn.cn/QaTL8 

    6.删除指定元素

            时间复杂度是O(n)

            删除指定元素,如果找到该元素,返回true;如果没有找到该元素,返回false。

    1. public boolean remove(Long e){
    2. MyNode cur=this.head;
    3. for(int i=0;i
    4. if(cur.val.equals(e)){
    5. //头删
    6. if(i==0){
    7. //size可能性>=1,分情况讨论
    8. this.head=this.head.next;
    9. // size > 1 则 此时 this.head != null
    10. if(this.head!=null){
    11. this.head.prev==null;
    12. //size == 1 则 此时 this.head == null
    13. }else{
    14. this.head=this.last=null;
    15. }
    16. this.size--;
    17. return true;
    18. }
    19. if(i==size-1){
    20. //size>1
    21. this.last=this.last.prev;
    22. this.last.next=null;
    23. this.size--;
    24. return true;
    25. }
    26. //既不是头删,也不是尾删
    27. MyNode prevNode =curNode.prev;
    28. MyNode nextNode =curNode.next;
    29. prevNode.next=nextNode;
    30. nextNode.next=prevNode;
    31. this.size--;
    32. return true;
    33. }
    34. cur=cur.next;//循环,写到最后别忘了
    35. }
    36. return false;
    37. }

    7.获取指定下标元素

            时间复杂度是0(n)

            首先要判断下标是非合法,然后遍历获取。

    1. public Long get(int index){
    2. if(index<0||index>=size){
    3. throw new ArrayIndexOutOfBoundsException("下标不合法");
    4. }
    5. MyNode curNode=this.head;
    6. for(int i=0;i//遍历
    7. curNode=curNode.next;
    8. }
    9. return curNode.val;
    10. }

    8.设置指定下标元素

            时间复杂度是0(n)

            首先要判断下标是非合法,然后遍历获取。存储旧的结点信息。

    1. public Long set(int index,Long e){
    2. if(index<0||index>=size){
    3. throw new ArrayIndexOutOfBoundsException("下标不合法");
    4. }
    5. MyNode curNode = this.head;
    6. for(int i=0;i//遍历
    7. curNode=curNode.next;
    8. }
    9. Long oldValue=curNode.val;//存储旧值
    10. curNode.val=e;
    11. return oldValue;
    12. }

    9.从前往后获取指定元素下标

    时间复杂度是O(n)

    1. public int indexOf(Long e){
    2. int i=0;
    3. MyNode curNode =this.head;
    4. while(curNode!=null){
    5. if(curNode.val.equals(e)){
    6. return i;
    7. }
    8. i++;
    9. curNode=curNode.next;
    10. }
    11. return -1;
    12. }

    10.从后往前获取指定元素下标

    时间复杂度是O(n)

    1. public int lastIndexOf(Long e){
    2. int i=size-1;
    3. MyNode curNode =this.last;
    4. while(curNode!=null){
    5. if(curNode.val.equals(e)){
    6. return i;
    7. }
    8. i--;
    9. curNode=curNode.prev;
    10. }
    11. return -1;
    12. }

    11.判断是否包含指定元素

    时间复杂度是O(n)

    1. public boolean contains(Long e){
    2. return indexOf(e)!=-1;
    3. }

    12.清空链表

    时间复杂度是O(1)

    1. public void clear() {
    2. this.head = this.last = null;
    3. this.size = 0;
    4. }

    13.判断链表是否为空

    时间复杂度是O(1)

    1. public boolean isEmpty() {
    2. return size == 0;
    3. }

    三、LinkedList的使用

    (1)LinkedList的构造

    方法解释
    LinkedList()无参构造
    public LinkedList(Collection c)使用其他集合容器中元素构造List
    1.  public static void main(String[] args) {
    2. // 构造一个空的LinkedList
    3. List list1 = new LinkedList<>();
    4. List list2 = new java.util.ArrayList<>();
    5. list2.add("A");
    6. list2.add("B");
    7. list2.add("C");
    8. // 使用ArrayList构造LinkedList
    9. List list3 = new LinkedList<>(list2);
    10. }

    (2)LinkedList的其他常用方法介绍

    方法 解释
    boolean add(E e)尾插 e
    void add(int index, E element) 将 e 插入到 index 位置
    boolean addAll(Collection c)尾插 c 中的元素
    E remove(int index)删除 index 位置元素
    boolean remove(Object o) 删除遇到的第一个 o
    E get(int index)获取下标 index 位置元素
    E set(int index, E element)将下标 index 位置元素设置为 element
    void clear()清空
    boolean contains(Object o)判断 o 是否在线性表中
    int indexOf(Object o) 返回第一个 o 所在下标
    int lastIndexOf(Object o) 返回最后一个 o 的下标
    List subList(int fromIndex, int toIndex)截取部分 list
    1. public static void main(String[] args) {
    2. LinkedList list = new LinkedList<>();
    3. list.add(1); // add(elem): 表示尾插
    4. list.add(2);
    5. list.add(3);
    6. list.add(4);
    7. list.add(5);
    8. list.add(6);
    9. list.add(7);
    10. System.out.println(list.size());
    11. System.out.println(list);
    12. // 在起始位置插入0
    13. list.add(0, 0); // add(index, elem): 在index位置插入元素elem
    14. System.out.println(list);
    15. list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
    16. list.removeFirst(); // removeFirst(): 删除第一个元素
    17. list.removeLast(); // removeLast(): 删除最后元素
    18. list.remove(1); // remove(index): 删除index位置的元素
    19. System.out.println(list);
    20. // contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
    21. if(!list.contains(1)){
    22. list.add(0, 1);
    23. }
    24. list.add(1);
    25. System.out.println(list);
    26. System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置
    27. System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置
    28. int elem = list.get(0); // get(index): 获取指定位置元素
    29. list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
    30. System.out.println(list);
    31. // subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
    32. List copy = list.subList(0, 3);
    33. System.out.println(list);
    34. System.out.println(copy);
    35. list.clear(); // 将list中元素清空
    36. System.out.println(list.size());
    37. }

    (3)LinkedList的遍历

            a.foreach遍历

            b.迭代器正向遍历

            c.迭代器负向遍历

    1. public static void main(String[] args) {
    2. LinkedList list = new LinkedList<>();
    3. list.add(1); // add(elem): 表示尾插
    4. list.add(2);
    5. list.add(3);
    6. list.add(4);
    7. list.add(5);
    8. list.add(6);
    9. list.add(7);
    10. System.out.println(list.size());
    11. // foreach遍历
    12. for (int e:list) {
    13. System.out.print(e + " ");
    14. }
    15. System.out.println();
    16. // 使用迭代器遍历---正向遍历
    17. ListIterator it = list.listIterator();
    18. while(it.hasNext()){
    19. System.out.print(it.next()+ " ");
    20. }
    21. System.out.println();
    22. // 使用反向迭代器---反向遍历
    23. ListIterator rit = list.listIterator(list.size());
    24. while (rit.hasPrevious()){
    25. System.out.print(rit.previous() +" ");
    26. }
    27. System.out.println();
    28. }

    四、ArrayList与LinkedList的比较

    不同点ArrayList LinkedList
    存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
    随机访问支持O(1)不支持:O(N)
    头插需要搬移元素,效率低O(N)只需修改引用的指向,时间复杂度为O(1)
    插入空间不够时需要扩容没有容量的概念
    应用场景元素高效存储+频繁访问任意位置插入和删除频繁

     

  • 相关阅读:
    2022年软件测试行业就业发展前景,软件测试前景好吗?我该学什么?
    OAuth 2.0一键登录那些事
    1.2 Hadoop简介-hadoop-最全最完整的保姆级的java大数据学习资料
    la3_系统调用(上)
    AD域管理-Active Directory批量用户管理
    c++后端开发书籍推荐
    Ajax异步请求(不等待,继续执行)
    蓝牙模块有哪些种类?BLE低功耗蓝牙模块有什么特点?
    MongoDB下载安装配置(windows版本)
    C语言之判断与循环语句知识点总结
  • 原文地址:https://blog.csdn.net/m0_63372226/article/details/127376121