尚硅谷图解Java数据结构和算法.pdf
图解.xlsx

1、单向链表

链表的介绍

链表在内存中的存储
二、链表 - 图1
特点

  • 链表是以节点的方式来存储,是链式存储
  • 每个节点包含 data 域 和 next 域。next域用来指向下一个节点
  • 链表的各个节点不一定是连续存储的
  • 链表分带头节点的链表没有头节点的链表,根据实际的需求来确定

带头结点的逻辑示意图
二、链表 - 图2

实现思路

创建(添加)

  • 先创建一个Head头节点,表示单链表的头
  • 后面我们每添加一个节点,就放在链表的最后

    遍历

  • 通过一个辅助变量,来遍历整个链表

image.png

有序插入

  • 先遍历链表,找到应该插入的位置
  • 要插入的节点的next指向插入位置的后一个节点
  • 插入位置的前一个节点的next指向要插入节点
    • 插入前要判断是否在队尾插入

image.png

根据某个属性节点修改值

  • 先遍历节点,找到修改的位置

    • 如果未找到修改节点,则不修改

      删除某个节点

  • 先遍历节点,找到要删除节点的前一个节点

  • 进行删除操作

image.png
代码

  1. package com.luyi.DataStructures.linkedlist;
  2. /**
  3. * @author 卢意
  4. * @create 2020-12-01 21:10
  5. */
  6. public class SingleLinkedListDemo {
  7. public static void main(String[] args) {
  8. // 先创建节点
  9. HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
  10. HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
  11. HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
  12. HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
  13. // 创建一个链表
  14. SingleLinkedList singleLinkedList = new SingleLinkedList();
  15. // 加入
  16. // singleLinkedList.add(heroNode1);
  17. // singleLinkedList.add(heroNode2);
  18. // singleLinkedList.add(heroNode3);
  19. // singleLinkedList.add(heroNode4);
  20. /**
  21. * 结果:
  22. * HeroNode{no=1, name='宋江', nickName='及时雨'}
  23. * HeroNode{no=2, name='卢俊义', nickName='玉麒麟'}
  24. * HeroNode{no=3, name='吴用', nickName='智多星'}
  25. * HeroNode{no=4, name='林冲', nickName='豹子头'}
  26. */
  27. // 加入按照编号的顺序
  28. singleLinkedList.addByOrder(heroNode1);
  29. singleLinkedList.addByOrder(heroNode4);
  30. singleLinkedList.addByOrder(heroNode2);
  31. singleLinkedList.addByOrder(heroNode3);
  32. singleLinkedList.addByOrder(heroNode3);
  33. /**
  34. * 结果
  35. * 准备插入的英雄的编号[3]已存在,不能加入!
  36. * HeroNode{no=1, name='宋江', nickName='及时雨'}
  37. * HeroNode{no=2, name='卢俊义', nickName='玉麒麟'}
  38. * HeroNode{no=3, name='吴用', nickName='智多星'}
  39. * HeroNode{no=4, name='林冲', nickName='豹子头'}
  40. */
  41. // 显示
  42. singleLinkedList.list();
  43. // 测试 修改节点
  44. HeroNode newHeroNode = new HeroNode(2, "小卢", "小麒麟");
  45. HeroNode newHeroNode1 = new HeroNode(5, "小卢", "小麒麟");
  46. singleLinkedList.update(newHeroNode );
  47. singleLinkedList.update(newHeroNode1 );
  48. /**
  49. * 结果
  50. * HeroNode{no=1, name='宋江', nickName='及时雨'}
  51. * HeroNode{no=2, name='卢俊义', nickName='玉麒麟'}
  52. * HeroNode{no=3, name='吴用', nickName='智多星'}
  53. * HeroNode{no=4, name='林冲', nickName='豹子头'}
  54. * 没有找到编号为[5]的节点
  55. * 修改后的链表情况
  56. *
  57. * HeroNode{no=1, name='宋江', nickName='及时雨'}
  58. * HeroNode{no=2, name='小卢', nickName='小麒麟'}
  59. * HeroNode{no=3, name='吴用', nickName='智多星'}
  60. * HeroNode{no=4, name='林冲', nickName='豹子头'}
  61. */
  62. // 修改后的显示
  63. System.out.println("修改后的链表情况");
  64. System.out.println();
  65. singleLinkedList.list();
  66. System.out.println("删除一个节点");
  67. singleLinkedList.delete(1);
  68. singleLinkedList.delete(2);
  69. singleLinkedList.delete(3);
  70. singleLinkedList.delete(4);
  71. singleLinkedList.list();
  72. /**
  73. * 结果
  74. * 删除一个节点
  75. * 链表为空
  76. */
  77. }
  78. }
  79. // 定义一个单向链表 管理我们的英雄
  80. class SingleLinkedList {
  81. // 初始化一下头结点 头结点不要动 不存放具体的数据
  82. private HeroNode head =new HeroNode(0,null,null);
  83. // 添加方法到单向链表
  84. // 思路 当不考虑编号顺序时
  85. // 1. 找到当前链表的最后节点
  86. // 2. 将最后的这个节点的next 指向 新节点
  87. public void add(HeroNode heroNode){
  88. // 因为 head节点不能动 我们需要一个辅助节点 temp
  89. HeroNode temp = head;
  90. // 遍历链表找到最后
  91. while (true){
  92. // 找到链表的最后
  93. if(temp.next == null){
  94. break;
  95. }
  96. // 如果没有找到 就将temp后移
  97. temp = temp.next;
  98. }
  99. // 当退出white循环时, temp 就指向了链表的最后
  100. temp.next = heroNode;
  101. }
  102. // 第二种添加英雄的方式, 根据排名将英雄插入到指定位置
  103. // 如果有这个排名, 则添加失败 并给出提示
  104. public void addByOrder(HeroNode heroNode){
  105. // 因为头结点不能动 因此我们任然通过一个辅助指针(变量) 来帮助找到添加的位置
  106. // 因为是单列表 temp是要找到添加节点的前一个节点, 否则插入不了
  107. HeroNode temp = head;
  108. boolean flag = false; // 标志添加的编号是否存在 默认为false
  109. while(true) {
  110. if(temp.next == null) { // 说明已经到了链表的最后了
  111. break;
  112. }
  113. if (temp.next.no > heroNode.no) { // 位置找到了 在temp和temp.next之间插入该节点
  114. break;
  115. }else if (temp.next.no == heroNode.no) { // 说明希望添加的heroNode的编号已经存在 不能添加
  116. flag = true; // 说明编号存在
  117. break;
  118. }
  119. temp = temp.next;
  120. }
  121. // 判断 flag的值
  122. if (flag){ // 不能添加 说明编号存在
  123. System.out.println("准备插入的英雄的编号["+heroNode.no+"]已存在,不能加入!");
  124. }else {
  125. // 插入链表中 temp 中
  126. heroNode.next = temp.next;
  127. temp.next = heroNode;
  128. }
  129. }
  130. // 显示链表 遍历
  131. public void list(){
  132. // 判断链表为空
  133. if(head.next == null){
  134. System.out.println("链表为空");
  135. return;
  136. }
  137. // 因为头结点不能动我们需要一个复制节点 temp
  138. HeroNode temp = head.next;
  139. while(true){
  140. // 输出这个节点的信息
  141. System.out.println(temp);
  142. if(temp.next == null){
  143. break;
  144. }
  145. // 将temp 后移 不然是一个死循环
  146. temp = temp.next;
  147. }
  148. }
  149. // 修改节点的信息 根据编号来修改 编号不能修改
  150. public void update (HeroNode newHeroNode){
  151. if(head.next == null){
  152. System.out.println("链表为空");
  153. return;
  154. }
  155. // 找到需要修改的节点 很具no查询
  156. HeroNode temp = head.next;
  157. boolean flag = false; // 表示是否找到该节点
  158. while (true) {
  159. if (temp == null) {
  160. break; // 表示链表已经遍历结束了 (不是最后一个节点 已经出到外面去了)
  161. }
  162. if (temp.no == newHeroNode.no){
  163. // 找到要修改的节点
  164. flag = true;
  165. break;
  166. }
  167. temp = temp.next;
  168. }
  169. // 根据flag判断是否找到要修改的节点
  170. if(!flag){
  171. System.out.println("没有找到编号为["+newHeroNode.no+ "]的节点");
  172. }else {
  173. temp.name = newHeroNode.name;
  174. temp.nickName = newHeroNode.nickName;
  175. }
  176. }
  177. // 删除节点
  178. //思路 1.head节点不能动 因此我们需要一个temp辅助节点找到删除节点的前一个节点
  179. // 2. 我们需要以temp节点的后一个节点的标号进行比较
  180. public void delete(int no){
  181. HeroNode temp = head;
  182. boolean flag = false; // 标志是否找到待删除节点的
  183. while (true) {
  184. if (temp.next == null){ // temp已经到链表的最后
  185. break;
  186. }
  187. if (temp.next.no == no){
  188. // 找到了待删除节点的前一个节点
  189. flag = true;
  190. break;
  191. }
  192. temp = temp.next;
  193. }
  194. if (flag){
  195. // 可以删除
  196. HeroNode t = temp.next;
  197. temp.next = t.next;
  198. t.next = null;
  199. }else {
  200. System.out.println("没有找到该no=["+no+"]节点,无法删除");
  201. }
  202. }
  203. }
  204. // 定义一个HeroNode , 每个HeroNode 对象就是一个节点
  205. class HeroNode {
  206. public int no;
  207. public String name;
  208. public String nickName;
  209. public HeroNode next; // 指向下一个节点
  210. // 构造器
  211. public HeroNode(int hNo, String hName, String hNickName){
  212. this.no = hNo;
  213. this.name = hName;
  214. this.nickName = hNickName;
  215. }
  216. // 为了显示方便 重写toString方法
  217. @Override
  218. public String toString() {
  219. return "HeroNode{" +
  220. "no=" + no +
  221. ", name='" + name + '\'' +
  222. ", nickName='" + nickName + '\'' +
  223. '}';
  224. }
  225. }

单链表面试题


image.png

求单链表的有效节点的个数

  1. /**
  2. *
  3. * @param head 链表的头节点
  4. * @return 返回的是有效节点个数
  5. */
  6. public Integer getLength(HeroNode head){
  7. if (head.next == null){
  8. // 空链表
  9. return 0;
  10. }
  11. int length = 0;
  12. // 定义一个辅助变量
  13. HeroNode cur =head.next;
  14. while (cur != null) {
  15. length++;
  16. cur = cur.next;
  17. }
  18. return length;
  19. }

求单链表的倒数第K个节点

  1. // 求单链表的倒数第K个节点
  2. // 思路 1 编写一个方法接收head节点 接收index(倒数第index节点)
  3. // 2 先把链表变历 求出长度 length 去找到length-index的节点
  4. public HeroNode findNodeStartLast(HeroNode head ,int index){
  5. // 如果链表为空 返回null
  6. if (head.next == null){
  7. return null;
  8. }
  9. // 变历得到链表的长度
  10. int length = this.getLength(head);
  11. // 再一次遍历 返回 第length - index
  12. // 先做一个index的校验
  13. if(index <= 0 || index > length){
  14. return null;
  15. }
  16. // 定义一个辅助节点
  17. HeroNode temp = head.next;
  18. for(int i = 0 ;i < length-index ;i++ ){
  19. temp = temp.next;
  20. }
  21. return temp;
  22. }

单链表的反转

image.png

  1. // 单链表的反转
  2. public HeroNode reverseNode(HeroNode head){
  3. //如果当前链表为空,或者只有一个节点,无需反转,直接返回
  4. if(head.next == null || head.next.next == null) {
  5. return head.next;
  6. }
  7. //定义一个辅助的指针(变量),帮助我们遍历原来的链表
  8. HeroNode cur = head.next;
  9. HeroNode next = null;// 指向当前节点[cur]的下一个节点
  10. HeroNode reverseHead = new HeroNode(0, "", "");
  11. //遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
  12. //动脑筋
  13. while(cur != null) {
  14. next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
  15. cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端
  16. reverseHead.next = cur; //将cur 连接到新的链表上
  17. cur = next;//让cur后移
  18. }
  19. //将head.next 指向 reverseHead.next , 实现单链表的反转
  20. head.next = reverseHead.next;
  21. return reverseHead.next;
  22. }

二、链表 - 图8

  • 创建一个新的头结点,作为新链表的头
  • 从头遍历旧链表,将遍历到的节点插入新链表的头结点之后
  • 注意需要用到两个暂存节点
    • 一个用来保存正在遍历的节点
    • 一个用来保存正在遍历节点的下一个节点

二、链表 - 图9
二、链表 - 图10
二、链表 - 图11
二、链表 - 图12
二、链表 - 图13

逆序打印

  • 遍历链表,将遍历到的节点入栈
  • 遍历完后,进行出栈操作,同时打印出栈元素

image.png

  1. //可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
  2. public static void reversePrint(HeroNode head) {
  3. if(head.next == null) {
  4. return;//空链表,不能打印
  5. }
  6. //创建要给一个栈,将各个节点压入栈
  7. Stack<HeroNode> stack = new Stack<HeroNode>();
  8. HeroNode cur = head.next;
  9. //将链表的所有节点压入栈
  10. while(cur != null) {
  11. stack.push(cur);
  12. cur = cur.next; //cur后移,这样就可以压入下一个节点
  13. }
  14. //将栈中的节点进行打印,pop 出栈
  15. while (stack.size() > 0) {
  16. System.out.println(stack.pop()); //stack的特点是先进后出
  17. }
  18. }

2、双向链表

单链表的问题

image.png

双向链表

二、链表 - 图16

实现思路

遍历

  • 和单向链表的遍历相同,需要一个辅助节点来保存当前正在遍历的节点

添加

  • 双向链表多出了一个frnot,所以在添加时,要让新增节点的front指向链表尾节点

修改

  • 和单向链表的修改相同

删除

  • 使用temp来保存要删除的节点
  • temp.pre.next指向temp.next
  • temp.next指向temp.pre

image.png
代码

  1. package com.atguigu.linkedlist;
  2. public class DoubleLinkedListDemo {
  3. public static void main(String[] args) {
  4. // 测试
  5. System.out.println("双向链表的测试");
  6. // 先创建节点
  7. HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");
  8. HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");
  9. HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星");
  10. HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头");
  11. // 创建一个双向链表
  12. DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
  13. doubleLinkedList.add(hero1);
  14. doubleLinkedList.add(hero2);
  15. doubleLinkedList.add(hero3);
  16. doubleLinkedList.add(hero4);
  17. doubleLinkedList.list();
  18. // 修改
  19. HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙");
  20. doubleLinkedList.update(newHeroNode);
  21. System.out.println("修改后的链表情况");
  22. doubleLinkedList.list();
  23. // 删除
  24. doubleLinkedList.del(3);
  25. System.out.println("删除后的链表情况~~");
  26. doubleLinkedList.list();
  27. }
  28. }
  29. // 创建一个双向链表的类
  30. class DoubleLinkedList {
  31. // 先初始化一个头节点, 头节点不要动, 不存放具体的数据
  32. private HeroNode2 head = new HeroNode2(0, "", "");
  33. // 返回头节点
  34. public HeroNode2 getHead() {
  35. return head;
  36. }
  37. // 遍历双向链表的方法
  38. // 显示链表[遍历]
  39. public void list() {
  40. // 判断链表是否为空
  41. if (head.next == null) {
  42. System.out.println("链表为空");
  43. return;
  44. }
  45. // 因为头节点,不能动,因此我们需要一个辅助变量来遍历
  46. HeroNode2 temp = head.next;
  47. while (true) {
  48. // 判断是否到链表最后
  49. if (temp == null) {
  50. break;
  51. }
  52. // 输出节点的信息
  53. System.out.println(temp);
  54. // 将temp后移, 一定小心
  55. temp = temp.next;
  56. }
  57. }
  58. // 添加一个节点到双向链表的最后.
  59. public void add(HeroNode2 heroNode) {
  60. // 因为head节点不能动,因此我们需要一个辅助遍历 temp
  61. HeroNode2 temp = head;
  62. // 遍历链表,找到最后
  63. while (true) {
  64. // 找到链表的最后
  65. if (temp.next == null) {//
  66. break;
  67. }
  68. // 如果没有找到最后, 将将temp后移
  69. temp = temp.next;
  70. }
  71. // 当退出while循环时,temp就指向了链表的最后
  72. // 形成一个双向链表
  73. temp.next = heroNode;
  74. heroNode.pre = temp;
  75. }
  76. //第二种方式在添加英雄时,根据排名将英雄插入到指定位置(按顺序添加)
  77. //(如果有这个排名,则添加失败,并给出提示)
  78. public void addByOrder(HeroNode2 heroNode){
  79. //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
  80. HeroNode2 temp = head;
  81. boolean flag = false;
  82. while (true){
  83. if (temp.next == null){//说明temp已经在链表的最后
  84. break;
  85. }
  86. if (temp.next.no > heroNode.no){//位置找到,就在temp的后面插入
  87. break;
  88. }else if (temp.next.no == heroNode.no){//说明希望添加的heroNode的编号已然存在
  89. flag = true; //说明编号存在
  90. break;
  91. }
  92. temp = temp.next; //后移,遍历当前链表
  93. }
  94. //判断flag 的值
  95. if (flag){//不能添加,说明编号存在
  96. System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.no);
  97. }else {
  98. //插入到链表中, temp的后面
  99. heroNode.next = temp.next;
  100. temp.next = heroNode;
  101. heroNode.pre = temp;
  102. }
  103. }
  104. // 修改一个节点的内容, 可以看到双向链表的节点内容修改和单向链表一样
  105. // 只是 节点类型改成 HeroNode2
  106. public void update(HeroNode2 newHeroNode) {
  107. // 判断是否空
  108. if (head.next == null) {
  109. System.out.println("链表为空~");
  110. return;
  111. }
  112. // 找到需要修改的节点, 根据no编号
  113. // 定义一个辅助变量
  114. HeroNode2 temp = head.next;
  115. boolean flag = false; // 表示是否找到该节点
  116. while (true) {
  117. if (temp == null) {
  118. break; // 已经遍历完链表
  119. }
  120. if (temp.no == newHeroNode.no) {
  121. // 找到
  122. flag = true;
  123. break;
  124. }
  125. temp = temp.next;
  126. }
  127. // 根据flag 判断是否找到要修改的节点
  128. if (flag) {
  129. temp.name = newHeroNode.name;
  130. temp.nickname = newHeroNode.nickname;
  131. } else { // 没有找到
  132. System.out.printf("没有找到 编号 %d 的节点,不能修改\n", newHeroNode.no);
  133. }
  134. }
  135. // 从双向链表中删除一个节点,
  136. // 说明
  137. // 1 对于双向链表,我们可以直接找到要删除的这个节点
  138. // 2 找到后,自我删除即可
  139. public void del(int no) {
  140. // 判断当前链表是否为空
  141. if (head.next == null) {// 空链表
  142. System.out.println("链表为空,无法删除");
  143. return;
  144. }
  145. HeroNode2 temp = head.next; // 辅助变量(指针)
  146. boolean flag = false; // 标志是否找到待删除节点的
  147. while (true) {
  148. if (temp == null) { // 已经到链表的最后
  149. break;
  150. }
  151. if (temp.no == no) {
  152. // 找到的待删除节点的前一个节点temp
  153. flag = true;
  154. break;
  155. }
  156. temp = temp.next; // temp后移,遍历
  157. }
  158. // 判断flag
  159. if (flag) { // 找到
  160. // 可以删除
  161. // temp.next = temp.next.next;[单向链表]
  162. temp.pre.next = temp.next;
  163. // 这里我们的代码有问题?
  164. // 如果是最后一个节点,就不需要执行下面这句话,否则出现空指针
  165. if (temp.next != null) {
  166. temp.next.pre = temp.pre;
  167. }
  168. } else {
  169. System.out.printf("要删除的 %d 节点不存在\n", no);
  170. }
  171. }
  172. }
  173. // 定义HeroNode2 , 每个HeroNode 对象就是一个节点
  174. class HeroNode2 {
  175. public int no;
  176. public String name;
  177. public String nickname;
  178. public HeroNode2 next; // 指向下一个节点, 默认为null
  179. public HeroNode2 pre; // 指向前一个节点, 默认为null
  180. // 构造器
  181. public HeroNode2(int no, String name, String nickname) {
  182. this.no = no;
  183. this.name = name;
  184. this.nickname = nickname;
  185. }
  186. // 为了显示方法,我们重新toString
  187. @Override
  188. public String toString() {
  189. return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
  190. }
  191. }

3、单向环形链表

约瑟夫问题

image.png

单向环形链表

单链表的尾节点指向首节点,即可构成单向环形链表
二、链表 - 图19

约瑟夫环

image.png
大致思路

  • 遍历链表找到指定位置的节点
  • 用一个front保存指定节点的前一个节点,方便删除
  • 当count==time时,删除此时正在遍历的节点,放入数组中,并将count的值初始化
  • 用一个变量loopTime记录已经出圈了几个人,当其等于length时则是最后一个节点,直接放入数组并返回数即可

image.png
代码

  1. package com.atguigu.linkedlist;
  2. public class Josepfu {
  3. public static void main(String[] args) {
  4. // 测试一把看看构建环形链表,和遍历是否ok
  5. CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
  6. circleSingleLinkedList.addBoy(125);// 加入5个小孩节点
  7. circleSingleLinkedList.showBoy();
  8. //测试一把小孩出圈是否正确
  9. circleSingleLinkedList.countBoy(10, 20, 125); // 2->4->1->5->3
  10. //String str = "7*2*2-5+1-5+3-3";
  11. }
  12. }
  13. // 创建一个环形的单向链表
  14. class CircleSingleLinkedList {
  15. // 创建一个first节点,当前没有编号
  16. private Boy first = null;
  17. // 添加小孩节点,构建成一个环形的链表
  18. public void addBoy(int nums) {
  19. // nums 做一个数据校验
  20. if (nums < 1) {
  21. System.out.println("nums的值不正确");
  22. return;
  23. }
  24. Boy curBoy = null; // 辅助指针,帮助构建环形链表
  25. // 使用for来创建我们的环形链表
  26. for (int i = 1; i <= nums; i++) {
  27. // 根据编号,创建小孩节点
  28. Boy boy = new Boy(i);
  29. // 如果是第一个小孩
  30. if (i == 1) {
  31. first = boy;
  32. first.setNext(first); // 构成环
  33. curBoy = first; // 让curBoy指向第一个小孩
  34. } else {
  35. curBoy.setNext(boy);//
  36. boy.setNext(first);//
  37. curBoy = boy;
  38. }
  39. }
  40. }
  41. // 遍历当前的环形链表
  42. public void showBoy() {
  43. // 判断链表是否为空
  44. if (first == null) {
  45. System.out.println("没有任何小孩~~");
  46. return;
  47. }
  48. // 因为first不能动,因此我们仍然使用一个辅助指针完成遍历
  49. Boy curBoy = first;
  50. while (true) {
  51. System.out.printf("小孩的编号 %d \n", curBoy.getNo());
  52. if (curBoy.getNext() == first) {// 说明已经遍历完毕
  53. break;
  54. }
  55. curBoy = curBoy.getNext(); // curBoy后移
  56. }
  57. }
  58. // 根据用户的输入,计算出小孩出圈的顺序
  59. /**
  60. *
  61. * @param startNo
  62. * 表示从第几个小孩开始数数
  63. * @param countNum
  64. * 表示数几下
  65. * @param nums
  66. * 表示最初有多少小孩在圈中
  67. */
  68. public void countBoy(int startNo, int countNum, int nums) {
  69. // 先对数据进行校验
  70. if (first == null || startNo < 1 || startNo > nums) {
  71. System.out.println("参数输入有误, 请重新输入");
  72. return;
  73. }
  74. // 创建要给辅助指针,帮助完成小孩出圈
  75. Boy helper = first;
  76. // 需求创建一个辅助指针(变量) helper , 事先应该指向环形链表的最后这个节点
  77. while (true) {
  78. if (helper.getNext() == first) { // 说明helper指向最后小孩节点
  79. break;
  80. }
  81. helper = helper.getNext();
  82. }
  83. //小孩报数前,先让 first 和 helper 移动 k - 1次
  84. for(int j = 0; j < startNo - 1; j++) {
  85. first = first.getNext();
  86. helper = helper.getNext();
  87. }
  88. //当小孩报数时,让first 和 helper 指针同时 的移动 m - 1 次, 然后出圈
  89. //这里是一个循环操作,知道圈中只有一个节点
  90. while(true) {
  91. if(helper == first) { //说明圈中只有一个节点
  92. break;
  93. }
  94. //让 first 和 helper 指针同时 的移动 countNum - 1
  95. for(int j = 0; j < countNum - 1; j++) {
  96. first = first.getNext();
  97. helper = helper.getNext();
  98. }
  99. //这时first指向的节点,就是要出圈的小孩节点
  100. System.out.printf("小孩%d出圈\n", first.getNo());
  101. //这时将first指向的小孩节点出圈
  102. first = first.getNext();
  103. helper.setNext(first); //
  104. }
  105. System.out.printf("最后留在圈中的小孩编号%d \n", first.getNo());
  106. }
  107. }
  108. // 创建一个Boy类,表示一个节点
  109. class Boy {
  110. private int no;// 编号
  111. private Boy next; // 指向下一个节点,默认null
  112. public Boy(int no) {
  113. this.no = no;
  114. }
  115. public int getNo() {
  116. return no;
  117. }
  118. public void setNo(int no) {
  119. this.no = no;
  120. }
  121. public Boy getNext() {
  122. return next;
  123. }
  124. public void setNext(Boy next) {
  125. this.next = next;
  126. }
  127. }

4、跳跃表

1、什么是跳跃表

跳跃链表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉排序树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好。
基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化(抛硬币)的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名)。所有操作都以对数随机化的时间进行。

2、跳跃表原理

查询链表的时间复杂度

搜索链表中的元素时,无论链表中的元素是否有序,时间复杂度都为O(n),如下图,搜索103需要查询9次才能找到该节点
二、链表 - 图22
但是能够提高搜索的其他数据结构,如:二叉排序树、红黑树、B树、B+树等等的实现又过于复杂。有没有一种相对简单,同时又能提搜索效率的数据结构呢,跳跃表就是这样一种数据结构。
Redis中使用跳跃表好像就是因为一是B+树的实现过于复杂,二是Redis只涉及内存读写,所以最后选择了跳跃表。

跳跃表实现——搜索

为了能够更快的查找元素,我们可以在该链表之上,再添加一个新链表,新链表中保存了部分旧链表中的节点,以加快搜索的速度。如下图所示
二、链表 - 图23
我们搜索元素时,从最上层的链表开始搜索。当找到某个节点大于目标值或其后继节点为空时,从该节点向下层链表搜寻,然后顺着该节点到下一层继续搜索。
比如我们要找103这个元素,则会经历:2->23->54->87->103
这样还是查找了5次,当我们再将链表的层数增高以后,查找的次数会明显降低,如下图所示。3次便找到了目标元素103
二、链表 - 图24
代码中实现的跳表结构如下图所示
一个节点拥有多个指针,指向不同的节点
二、链表 - 图25

跳跃表实现——插入

跳跃表的插入策略如下

  • 先找到合适的位置以便插入元素
  • 找到后,将该元素插入到最底层的链表中,并且抛掷硬币(1/2的概率)
    • 若硬币为正面,则将该元素晋升到上一层链表中,并再抛一次
    • 若硬币为反面,则插入过程结束
  • 为了避免以下情况,需要在每个链表的头部设置一个 负无穷 的元素

二、链表 - 图26
设置负无穷后,若要查找元素2,过程如下图所示
二、链表 - 图27
插入图解

  • 若我们要将45插入到跳跃表中二、链表 - 图28
  • 先找到插入位置,将45插入到合适的位置二、链表 - 图29
  • 抛掷硬币:为正,晋升二、链表 - 图30
  • 假设硬币一直为正,插入元素一路晋升,当晋升的次数超过跳跃表的层数时,需要再创建新的链表以放入晋升的插入元素。新创建的链表的头结点为负无穷二、链表 - 图31

以上便是跳跃表的插入过程