• 单链表的基本操作


    目录

    一、什么是单链表?

    二、顺序表和链表的比较:

    1.空间性能的比较:

    2.时间性能的比较:

    三、单链表的实现和基本操作

    1.实现单链表节点以及初始化头结点

    2.获取当前链表中元素的数量

    3.获取指定位置的节点

    4.获取指定位置的元素

    5.在尾部添加结点元素

    6.在指定位置添加元素

    7.删除指定位置的元素

    四、反转链表

    1.用反转头实现 

    2.用递归实现反转链表

    五、用快慢指针获取链表中的值

    1.快慢指针获取链表中间的值

    2.通过快慢指针获取倒数第k个结点值

    六、循环链表

    1.通过快慢指针判断链表是否有环

    2.通过快慢指针以及第三个指针获取环的入口元素

    七、约瑟夫问题

    一、什么是单链表

    链表的存储结构在逻辑上、概念上的连续结构,但在实际的物理存储上是非连续、非顺序的。

    它的存储单元以结点为单位,每一个结点由数据域指针域两个域组成。数据域存储数据元素信息data,指针域存储直接后继存储位置next。

    整个链表的存取必须从头指针开始进行,头指针指链表中第一个结点(即第一个数据元素的存储映像,也称首元结点)的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针为空(null)

    头指针有两种情况:

    (1)为了方便处理,在单链表的第一个结点(首元结点)之前附设一个结点,称为头节点,头节点一般不放数据,所以data = null;

    (2)将第一个存放数据的节点(首元结点)的指针作为头指针。

    二、顺序表和链表的比较:

    1.空间性能的比较:

    (1)存储空间的分配:

    顺序表是用数组实现的,存储空间必须预先分配,元素个数扩充受一定的限制,容易造成存储空间浪费或空间溢出现象。而链表不需要为它预先分配空间,只要内存空间允许,链表中的元素个数就没有限制。

    因此,当线性表的长度变化较大,难以预估存储规模时,适合采用链表作为存储结构。

    (2)存储密度的大小:

    存储密度=数据元素本身占用的存储量 / 结点结构占用的存储量

    链表要存储数据域和指针域,所以存储密度小于1;顺序表只存储数据,所以存储密度为1;

    因此,当线性表的长度变化不大,提前知道其大小的时候,为节约存储空间,适合采用顺序表(数组)作为存储结构。

    2.时间性能的比较:

    (1)查找元素的效率:

    顺序表是由数组实现的,存储位置连续,有下标索引,所以查找取值操作顺序表时间复杂度为O(1)

    链表是逻辑上的连续,物理上不连续,查找元素只能从表头开始依次向后遍历链表,所以链表时间复杂度为O(n)。

    (2)插入和删除操作的效率:

    对于链表,插入和删除只需要修改指针,不涉及元素的移动,所以时间复杂度为O(1)。

    对于顺序表,插入和删除涉及到后面元素的移动,所以时间复杂度为O(n)。

    三、单链表的实现和基本操作

    1.实现单链表节点以及初始化头结点

    1. //单向链表的实现以及基本操作
    2. public class MySingleLinkedList {
    3. //节点类设计,设计结点内部类
    4. private class Node{
    5. T item;//结点存储的数据
    6. Node next;//下一个结点的地址
    7. //节点类设计
    8. public Node(T item, Node next) {
    9. this.item = item;
    10. this.next = next;
    11. }
    12. }
    13. //成员变量
    14. Node head;//存储链表的起始位置
    15. int size;//记录链表中的元素数量
    16. //构造方法初始化头结点
    17. public MySingleLinkedList() {
    18. //将头部结点进行初始化
    19. this.head = new Node(null,null);
    20. //将size初始化
    21. this.size = 0;
    22. }
    23. }

    2.获取当前链表中元素的数量

    1. //获取当前链表中元素的数量
    2. public int size(){
    3. return this.size;
    4. }

    3.获取指定位置的节点

    1. //获取指定位置的节点
    2. //思路:从链表首元结点开始(头结点后第一个结点)依次往后遍历,
    3. // 直到到达目标元素位置,将结点返回。
    4. public Node getNode(int pos){
    5. //当传入的值为-1时,就return头结点。一般在0结点插入时会出现此情况。
    6. if (pos == -1){
    7. return this.head;
    8. }
    9. //获取0位置的元素
    10. Node target = this.head.next;
    11. //通过遍历找到指定的位置的元素
    12. for(int i = 0;i
    13. target = target.next;
    14. }
    15. //返回结果
    16. return target;
    17. }

    4.获取指定位置的元素

    1. //获取指定位置的元素
    2. //思路:用getNode()方法获取目标结点,然后返回结点元素
    3. public T get(int pos){
    4. return getNode(pos).item;
    5. }

    5.在尾部添加结点元素

    1. //在尾部添加结点元素
    2. //思路:如果链表没有元素,直接将头结点的指针指向添加元素,
    3. // 如果链表已有元素,先到达尾结点,再将尾结点指针指向添加元素。
    4. public void add(T t){
    5. //创建插入的结点
    6. Node node = new Node(t,null);
    7. if (this.size == 0){
    8. //size == 0说明链表还没有元素,直接将头节点指针指向插入节点
    9. this.head.next = node;
    10. }else {
    11. //size != 0,先找到尾结点,再插入
    12. this.getNode(size - 1).next = node;
    13. }
    14. //将链表中结点的数量加1
    15. this.size++;
    16. }

    6.在指定位置添加元素

    1. //在指定位置添加元素
    2. //思路:先获取到指定位置结点,和前一个结点,
    3. // 将前一个结点的指针指向要插入的结点,再将插入节点的指针指向指定位置的结点。
    4. public void add(int pos,T t){
    5. //获取需要插入节点
    6. Node node = new Node(t,null);
    7. //根据pos获取插入点的结点
    8. Node current = this.getNode(pos);
    9. //获取pos前面位置的结点
    10. Node before = this.getNode(pos - 1);
    11. //将before的next节点指向需要插入的结点
    12. before.next = node;
    13. //将插入节点的next指向当前pos上的结点
    14. node.next = current;
    15. //将链表中结点的数量加1
    16. this.size++;
    17. }

    7.删除指定位置的元素

    1. //删除指定位置的元素
    2. //思路:获取到需要删除结点的位置,将前一个结点指针指向目标节点的指针。
    3. public T remove(int pos){
    4. //获取当前位置的前面一个节点
    5. Node before = this.getNode(pos - 1);
    6. //获取当前位置的结点
    7. Node current = this.getNode(pos);
    8. //当前位置前面的结点的next指向当前位置结点的next,(即当前位置下一个结点)
    9. before.next = current.next;
    10. //将将链表中结点的数量减1
    11. this.size--;
    12. return current.item;
    13. }

    四、反转链表

    1.用反转头实现 

    思路:先创建一个反转头结点,然后将正序链表中的结点从前到后依次放入反转链表中,
    //     同时,每次将结点放入反转链表都是插入到第一个结点,与反转头节点直接相连,
    //     这里用循环逐个插入,最后将现有的头结点指向反转后的第一个结点即可。

    1. //链表的反转
    2. //思路:先创建一个反转头结点,然后将正序链表中的结点从前到后依次放入反转链表中,
    3. // 同时,每次将结点放入反转链表都是插入到第一个结点,与反转头节点直接相连,
    4. // 这里用循环逐个插入,最后将现有的头结点指向反转后的第一个结点即可。
    5. public void reverse(){
    6. //创建一个反转的头结点
    7. Node reverseHeader = new Node(null,null);
    8. //获取正序链表的第一个结点
    9. Node first = this.head.next;
    10. //循环执行
    11. while(first != null){
    12. //将原有的头结点指向first的next结点
    13. this.head.next = first.next;
    14. //将反转的next赋值给first的next
    15. first.next = reverseHeader.next;
    16. //将reverseHeader指向first
    17. reverseHeader.next = first;
    18. //从正序链表中获取第一个元素
    19. first = this.head.next;
    20. }
    21. //将现有头结点的next指向反转头节点的next,也就是指向反转后的首元结点
    22. this.head.next = reverseHeader.next;
    23. }

    2.用递归实现反转链表

    思路:递归反转就是从原链表的第一个存储节点开始,依次递归调用反转每一个结点,
    //     直到把最后一个反转完毕,真个链表就反转完毕了。

    1. //2.用递归实现反转链表
    2. //思路:递归反转就是从原链表的第一个存储节点开始,依次递归调用反转每一个结点,
    3. // 直到把最后一个反转完毕,真个链表就反转完毕了。
    4. public Node reverse2(Node curr){
    5. //判断是否存在下一个结点
    6. if(curr.next != null){
    7. //通过反转下一个结点,获取prev
    8. Node prev = reverse2(curr.next);
    9. //将之前的结点的next结点设置成当前结点
    10. prev.next = curr;
    11. //返回curr结点
    12. }else{
    13. //当前结点没有下一个结点
    14. //将头部结点的next指向当前结点
    15. this.head.next = curr;
    16. }
    17. return curr;
    18. }
    19. //调用reverse2
    20. public void getReverse2(){
    21. reverse2(this.head.next);
    22. }

    五、用快慢指针获取链表中的值

    1.快慢指针获取链表中间的值

    思路:定义两个指针,一个快指针,一个慢指针,
    //     快指针每次移动两个节点,慢指针每次移动一个节点
    //     当快指针移动到链表尾部时,慢指针刚好移动到链表中间

      

    1. //1.通过快慢指针获取中间的元素的值
    2. //思路:定义两个指针,一个快指针,一个慢指针,
    3. // 快指针每次移动两个节点,慢指针每次移动一个节点
    4. // 当快指针移动到链表尾部时,慢指针刚好移动到链表中间,将结点值返回即可
    5. public T getRKItem1(){
    6. //定义一个快指针
    7. Node fast = this.head.next;
    8. //定义一个慢指针
    9. Node slow = this.head.next;
    10. //同时移动快慢指针,快指针每次走2个节点,直到快指针到达链表尾部
    11. while(fast != null && fast.next != null){
    12. //移动慢指针
    13. slow = slow.next;
    14. //移动快指针
    15. fast = fast.next;
    16. fast = fast.next;
    17. }
    18. return slow.item;
    19. }

    2.通过快慢指针获取倒数第k个结点值

    思路:定义两个指针,一个快指针,一个慢指针,
    //     快指针先移动到k-1的位置,慢指针每次移动一个节点
    //     当快指针移动到链表尾部时,慢指针刚好移动到链表中间,将结点值返回即可

    1. //2.通过快慢指针获取倒数K位置的值
    2. //思路:定义两个指针,一个快指针,一个慢指针,
    3. // 快指针先移动到k-1的位置,慢指针每次移动一个节点
    4. // 当快指针移动到链表尾部时,慢指针刚好移动到链表中间,将结点值返回即可
    5. public T getRKItem2(int k){
    6. //定义一个快指针
    7. Node fast = this.head.next;
    8. //定义一个慢指针
    9. Node slow = this.head.next;
    10. //先将快指针移动到k-1的位置
    11. for(int i = 0;i1;i++){
    12. fast = fast.next;
    13. }
    14. //同时移动快慢指针,直到快指针移动到链表的尾部
    15. while(fast.next != null){
    16. //移动快指针
    17. fast = fast.next;
    18. //移动慢指针
    19. slow = slow.next;
    20. }
    21. return slow.item;
    22. }

    六、循环链表

    1.通过快慢指针判断链表是否有环

    思路:定义两个指针,一个快指针,一个慢指针,
    //     快指针每次移动两个节点,慢指针每次移动一个节点
    //     如果链表有环,快指针和慢指针会在某次移动时相遇,表示有环。
    //     相遇则返回true,不相遇则返回false 

    1. //通过快慢指针判断链表是否有环
    2. //思路:定义两个指针,一个快指针,一个慢指针,
    3. // 快指针每次移动两个节点,慢指针每次移动一个节点
    4. // 如果链表有环,快指针和慢指针会在某次移动时相遇,表示有环。
    5. // 相遇则返回true,不相遇则返回false
    6. public boolean hasCircle(){
    7. //定义一个快指针
    8. Node fast = this.head.next;
    9. //定义一个慢指针
    10. Node slow = this.head.next;
    11. while (fast != null && fast.next != null){
    12. //移动快指针
    13. fast = fast.next.next;
    14. //移动慢指针
    15. slow = slow.next;
    16. //判断快指针和慢指针是否重合,从而判断出是否有环
    17. if (fast != null && fast.equals(slow)){
    18. return true;
    19. }
    20. }
    21. return false;
    22. }

    2.通过快慢指针以及第三个指针获取环的入口元素

    思路:先定义两个指针,一个快指针,一个慢指针,判断是否有环的循环里定义entry指针
    //     快指针每次移动两个节点,慢指针每次移动一个节点
    //     如果链表有环,快指针和慢指针会在某次移动时相遇,表示有环,
    //     然后在判断中创建entry指针,entry指针和slow指针从头往后开始遍历,直到相遇即为入口。
    //     相遇则返回true,不相遇则返回false

    1. //通过快慢指针判断链表是否有环
    2. //思路:先定义两个指针,一个快指针,一个慢指针,判断是否有环的循环里定义entry指针
    3. // 快指针每次移动两个节点,慢指针每次移动一个节点
    4. // 如果链表有环,快指针和慢指针会在某次移动时相遇,表示有环,
    5. // 然后在判断中创建entry指针,entry指针和slow指针从头往后开始遍历,直到相遇即为入口。
    6. // 相遇则返回true,不相遇则返回false
    7. public T hasCircleEntry(){
    8. //定义一个快指针
    9. Node fast = this.head.next;
    10. //定义一个慢指针
    11. Node slow = this.head.next;
    12. while (fast != null && fast.next != null){
    13. //移动快指针
    14. fast = fast.next.next;
    15. //移动慢指针
    16. slow = slow.next;
    17. //判断快指针和慢指针是否重合,从而判断出是否有环
    18. if (fast != null && fast.equals(slow)){
    19. //定义entry指针
    20. Node entry = this.head.next;
    21. while (!entry.equals(slow)){
    22. //entry指针向后移动
    23. entry = entry.next;
    24. //慢指针向后移动
    25. slow = slow.next;
    26. }
    27. //环的入口元素
    28. return entry.item;
    29. }
    30. }
    31. return null;
    32. }

    七、约瑟夫问题

    思路:先将元素添加到链表中,然后建成环装链表,
    //     定义一个游标target,依次往后移动,如果遇到要移除的元素,
    //     就将此结点的前一个结点的指针指向元素下一个结点,
    //     定义一个count用来计数,按指定的间隔数计数,每移除一个元素count返回到初始值。
    

     

     

    1. //解决约瑟夫问题,通过环形链表解决问题
    2. //思路:先将元素添加到链表中,然后建成环装链表,
    3. // 定义一个游标target,依次往后移动,如果遇到要移除的元素,
    4. // 就将此结点的前一个结点的指针指向元素下一个结点,
    5. // 定义一个count用来计数,按指定的间隔数计数,每移除一个元素count返回到初始值。
    6. //m是圈的总数
    7. //n是每次移除元素间隔的数量
    8. public void jsfKill(int m,int n){
    9. //构造一个循环链表
    10. for (int i = 1;i<=m;i++){
    11. this.add((T)(i+""));
    12. }
    13. //找到最后一个元素
    14. Node last = this.getNode(this.size - 1 );
    15. //建立环
    16. last.next = this.head.next;
    17. //
    18. //解决约瑟夫问题
    19. //定义一个游标
    20. Node target = this.head.next;
    21. //定义一个计时器
    22. int count = 1;
    23. //判断条件:当target == target.next时,即最后一个节点指针指向自己
    24. while(target != target.next){
    25. //将前一个元素的地址进行保存
    26. Node prev = target;
    27. //游标向后移动
    28. target = target.next;
    29. //计数
    30. /**
    31. * 这里自增自减有个知识点:
    32. * 1.如果一条语句中只有自增自减操作(如count++;),不管是++count还是count++,count的值都加一了
    33. * 2.如果一条语句中有其他变量(如int b = count++),那赋值遵循一个原则,即:
    34. * ++count:先自增后引用(count先加一,再赋值给b),count++:先引用后自增(count先赋值给b,再加一)
    35. */
    36. count++;
    37. //判断当前元素是否需要被移除
    38. if (count == n){
    39. System.out.println("移除的元素是:"+target.item);
    40. //将前面元素的next指向target的next
    41. prev.next = target.next;
    42. //将target也指向next的元素
    43. target = target.next;
    44. //重新开始计数
    45. count = 1;
    46. }
    47. }
    48. System.out.println("最后剩下元素是:"+target.item);
    49. }
  • 相关阅读:
    抖音团购跟小程序团购小程序开发有什么区别?
    2022亚太杯建模B题思路 : 高速列车的优化设计 小美赛数学建模 B题思路
    docker-compose 部署rabbitmq 15672打不开
    steam卡价越来越高,steam搬砖项目还能玩么?
    mac支持的硬盘格式 什么硬盘格式是mac和win支持的
    JS深拷贝处理日期、正则以及循环引用问题
    【你不知道的javascript上】this指针,函数的call、apply、bind,new 操作符到底发生了什么
    elasticsearch集成springboot详细使用
    亚马逊、wish、temu如何掌握自养号测评技术?
    力扣 SQL题目
  • 原文地址:https://blog.csdn.net/m0_51697147/article/details/127775549