当前位置:   article > 正文

单链表的基本操作_初始化单链表

初始化单链表

目录

一、什么是单链表?

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

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<T> {
  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<pos;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;i<k-1;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. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/636621
推荐阅读
相关标签
  

闽ICP备14008679号