待解决的问题

  • 单向环形链表
  • 单链表的反转
  • [ ] 双向链表的删除操作存在bug

    数据结构与算法概述

    什么是数据结构?

    数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据

    数据结构分类

    image.png
    image.png

    逻辑结构

    逻辑结构是从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互关系进行分类。

  • 集合结构:集合结构中的数据元素除了属于同一个集合外,他们之间没有其他任何的关系。

image.png

  • 线性结构:线性结构中的数据元素之间存在一对一的关系。

image.png

  • 树型结构:树型结构中的数据元素之间存在一对多的层次关系。

image.png

  • 图型结构:图形结构的数据元素是多对多的关系。

image.png

物理结构/存储结构

逻辑结构在计算机中真正的表示方式(又称为映像)称为物理结构,也可以叫做存储结构。

  • 顺序存储结构:把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的。例如数组就是顺序存储结构。

image.png

  • 链式存储结构:把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据元素之间并不能反映元素间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。

image.png

什么是算法?

算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法解决问题的策略机制。

算法分析

算法的时间复杂度分析

  • 事后分析估算法:在计算机程序编写后,利用计算机计时器对不同算法编写的程序运行时间进行比较,从而确定算法效率的高低。这种方式有很大的缺陷,写了半天发现这个算法不好,非常浪费时间,而且不同的测试环境的差别导致测试的结果差异也很大。
  • 事前分析估算法:在计算机程序编写前,依据统计方法对算法进行估算。

一个高级语言编写的程序程序在计算机上运行所消耗的时间取决于下列因素

  • 算法采用的策略和方案
  • 编译产生的代码质量
  • 问题的输入规模
  • 机器执行指令的速度

抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。如果算法固定,那么该算法的执行时间就只和问题的输入规模有关系了。

算法的时间复杂度分析规则

  1. 算法函数中的常数可以忽略(2n^3+3n+1)
  2. 算法函数中最高次幂的常数因子可以忽略(2n^3+3n+1)
  3. 算法函数中最高次幂越小,算法效率越高。(2n^3+3n+1)

    大O记法

    前提:执行次数=执行时间
    ⭐规则:

  4. 用 常数1 取代运行时间中的所有加法常数。

  5. 在第一步的基础上,只保留高阶项。
  6. 如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数。

    常见的大O阶

  7. 线性阶 O(n)

一般含有非嵌套循环涉及线性阶,线性阶就是随着输入规模的扩大,对应计算次数呈直线增长。
image.png

  1. 平方阶 O(n2)

一般嵌套循环属于这种时间复杂度。
image.png

  1. 立方阶 O(n3)

一般三层嵌套循环属于这种时间复杂度。
image.png

  1. 对数阶 O(logn)

对数阶,由于随着输入规模n的增大,不管底数为多少,他们的增长趋势是一样的,所以我们会忽略底数。
image.png

  1. 常数阶 O(1)

一般不涉及循环操作的都是常数阶,因为它不会随着n的增长而增加操作次数。
image.png
总结:
image.png
复杂度从低→高:O(1)我们的算法,尽可能的追求的是O(1),O(logn),O(n),O(nlogn)这几种时间复杂度,而如果发现算法的时间复杂度为平方阶、立方阶或者更复杂的,那我们可以分为这种算法是不可取的,需要优化。

最坏情况

除非特别指定,我们提到的运行时间都指的是最坏情况下的运行时间。
例:有一个存储了n个随机数字的数组,请从中查找出指定的数字。
image.png
最好情况:查找的第一个数字就是期望的数字,那么算法的时间复杂度为O(1)。
最坏情况:查找的最后一个数字,才是期望的数字,那么算法的时间复杂度为O(n)。
平均情况:任何数字查找的平均成本是O(n/2)。

算法的空间复杂度分析(了解)

在早期,算法在运行过程中对内存的占用情况也是一个经常需要考虑的问题。我们可以用算法的空间复杂度来描述算法对内存的占用。

  1. 基本数据类型的内存占用情况

image.png

  1. 计算机访问内存的方式都是一次一个字节。
  2. 一个引用需要8个字节来表示。
    1. Date d1 = new Date(),则d1这个变量需要占用8个字节来表示
  3. 创建一个对象,例如new Date(),除了对象内部存储的数据占用内存,该对象的本身也有内存开销,每个对象占用16个字节,用来保存对象的头信息。
  4. 在内存的使用中,如果不够8个字节,则会被自动填充为8字节。

image.png

  1. java中数组被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存)。

    数组

    基本介绍

    数组对应的英文是array, 是有限个相同类型的变量所组成的有序集合, 数组中的每一个变量被称为元素。 数组是最为简单、 最为常用的数据结构。
    image.png

  2. 数组的每一个元素也有着自己的下标, 下标从0开始, 一直到数组长度-1。

    数组的基本操作

    插入操作

    数组的实际元素数量有可能小于数组的长度 。
    image.png
    因此,数组的插入操作存在3中情况:

  • 尾部插入
  • 中间插入
  • 超范围插入

    尾部插入

    image.png
  1. 时间复杂度为O(1),直接将元素放入数组尾部的空闲位置即可。

    中间插入

    image.png
    由于数组的每一个元素都有其固定下标, 所以不得不首先把插入位置及后面的元素向后移动, 腾出地方, 再把要插入的元素放到对应的数组位置上。
    代码: ```java package ch00_array;

/**

  • 数组的中间插入操作 */ public class ArrayIns { private int[] array; private int size;

    public ArrayIns(int capacity) {

    1. //初始化数组大小
    2. this.array = new int[capacity];
    3. /*
    4. 数组中元素的个数
    5. 不是length,length是长度,比如这个数组的length就是10!
    6. */
    7. size = 0;

    }

    /**

    • 数组插入元素 *
    • @param element 插入的元素
    • @param index 插入位置的下标 */ public void insert(int element, int index) { //判断访问下标index是否越界 if (index < 0 || index > size) {

      1. throw new IndexOutOfBoundsException("数组下标越界!");

      } / 从后向前循环,将元素逐个向右移动一位 size=index,尾部插入 size>index,中间或头部插入 / for (int i = size; i >= index; i—) {

      1. array[i + 1] = array[i];

      } //将插入的元素放入腾出的空位置中 array[index] = element; //元素+1 size++; }

      /**

    • 数组的遍历 */ public void output() { for (int i = 0; i < size; i++) {

      1. System.out.println(array[i]);

      } }

      public static void main(String[] args) { ArrayIns arrayIns = new ArrayIns(10); arrayIns.insert(3, 0); arrayIns.insert(4, 1); arrayIns.insert(5, 2); arrayIns.insert(6, 3);

      arrayIns.insert(6, 1); arrayIns.output(); }

}

  1. <a name="y4AU7"></a>
  2. #### 超范围插入
  3. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2858485/1637292469192-759f0c28-6f69-4979-a6a9-877113cb77c7.png#clientId=ufbedb7f8-4f62-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=231&id=u79671163&margin=%5Bobject%20Object%5D&name=image.png&originHeight=461&originWidth=1059&originalType=binary&ratio=1&rotation=0&showTitle=false&size=237005&status=done&style=stroke&taskId=u5edd792a-e8fa-4e1d-a6f3-61802cc2f8c&title=&width=529.5)<br />如图所示,当有一个长度为6的数组,已经装满了元素,此时想插入一个新元素。这就涉及到了超范围插入。<br /> **解决方案:**<br />创建一个新数组, 长度是旧数组的2倍, 再把旧数组中的元素统统复制过去, 这样就实现了数组的扩容。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2858485/1637292870013-fc4e299b-4e98-4bdc-8485-13284e37bf7c.png#clientId=ufbedb7f8-4f62-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=236&id=ua5c2ab34&name=image.png&originHeight=472&originWidth=1031&originalType=binary&ratio=1&rotation=0&showTitle=false&size=278427&status=done&style=stroke&taskId=u77efeea2-7017-4c5d-a53f-6e8484871e4&title=&width=515.5)<br />**代码:**
  4. ```java
  5. package ch00_array;
  6. /**
  7. * 数组的中间插入操作
  8. */
  9. public class ArrayIns {
  10. private int[] array;
  11. private int size;
  12. public ArrayIns(int capacity) {
  13. //初始化数组大小
  14. this.array = new int[capacity];
  15. /*
  16. 数组中元素的个数
  17. 不是length,length是长度,比如这个数组的length就是10!
  18. */
  19. size = 0;
  20. }
  21. /**
  22. * 数组插入元素
  23. *
  24. * @param element 插入的元素
  25. * @param index 插入位置的下标
  26. */
  27. public void insert(int element, int index) {
  28. //判断访问下标index是否越界
  29. if (index < 0 || index > size) {
  30. throw new IndexOutOfBoundsException("数组下标越界!");
  31. }
  32. //判断数组元素个数是否达到容量上限,若达到则扩容
  33. if (size >= array.length) {
  34. resize();
  35. }
  36. /*
  37. 从后向前循环,将元素逐个向右移动一位
  38. size=index,尾部插入
  39. size>index,中间或头部插入
  40. */
  41. for (int i = size-1; i >= index; i--) {
  42. array[i + 1] = array[i];
  43. }
  44. //将插入的元素放入腾出的空位置中
  45. array[index] = element;
  46. //元素+1
  47. size++;
  48. }
  49. /**
  50. * 数组的扩容
  51. * arraycopy(Object src, int srcPos, Object dest, int destPos,int length);
  52. * src:要复制的数组(源数组)
  53. * srcPos:复制源数组的起始位置
  54. * dest:目标数组
  55. * destPos:目标数组的下标位置
  56. * length:要复制的长度
  57. */
  58. private void resize() {
  59. int[] newArray = new int[array.length * 2];
  60. System.arraycopy(array, 0, newArray, 0, array.length);
  61. array = newArray;
  62. }
  63. /**
  64. * 数组的遍历
  65. */
  66. public void output() {
  67. for (int i = 0; i < size; i++) {
  68. System.out.println(array[i]);
  69. }
  70. System.out.println("数组长度"+array.length);
  71. }
  72. public static void main(String[] args) {
  73. ArrayIns arrayIns = new ArrayIns(4);
  74. arrayIns.insert(3, 0);
  75. arrayIns.insert(4, 1);
  76. arrayIns.insert(5, 2);
  77. arrayIns.insert(6, 3);
  78. //超范围插入
  79. arrayIns.insert(6, 1);
  80. arrayIns.output();
  81. }
  82. }

删除操作

image.png
数组的删除操作和插入操作的过程相反, 如果删除的元素位于数组中间, 其后的元素都需要向前挪动1位由于不涉及扩容问题, 所以删除操作的代码实现比插入操作要简单。
代码:

  1. /**
  2. * 数组删除元素操作
  3. *
  4. * @param index
  5. * @return
  6. */
  7. public int delete(int index) {
  8. if (index < 0 || index >= size) {
  9. throw new IndexOutOfBoundsException("超出数组实际元素范围!");
  10. }
  11. int deleteElement = array[index];
  12. //从前向后循环,将元素逐个向左移动一位
  13. for (int i = index; i < size - 1; i++) {
  14. array[i] = array[i + 1];
  15. }
  16. size--;
  17. return deleteElement;
  18. }

基本介绍

栈是限制插入和删除只能在一个位置上进行的线性表
image.png

  • 允许插入和删除的一端称为末端,叫做栈顶(top)。
  • 不允许插入和删除的另一端叫做栈底(bottom)。
  • push(压栈),相当于表的插入操作(向栈顶插入一个元素)。
  • pop(弹栈),相当于表的删除操作(删除栈顶的一个元素)。
  • 栈是一种 后进先出(LIFO)的数据结构。

    实现栈的方式

    由于栈是一个表,因此任何实现表的方法都可以用来实现栈。主要有两种方式,链表实现和数组实现。

    链表实现栈

    可以使用单链表来实现栈。通过在表顶端插入一个元素来实现 PUSH,通过删除表顶端元素来实现 POP。使用链表方式实现的栈又叫动态栈。动态栈有链表的部分特性,即元素与元素之间在物理存储上可以不连续,但是功能有些受限制,动态栈只能在栈顶处进行插入和删除操作,不能在栈尾或栈中间进行插入和删除操作。

    数组实现栈

    栈也可以用数组来实现。使用数组方式实现的栈叫静态栈

    栈的快速入门

    使用数组来模拟栈的使用

    ```java package ch01_stack;

/**

  • 使用数组模拟栈 */ public class ArrayStack { //栈的大小 private int maxStack; //数组模拟栈 private int[] stack; //表示栈顶,默认值为-1(空栈)。 private int top = -1;

    //初始化栈的大小 public ArrayStack(int maxStack) {

    1. this.maxStack = maxStack;
    2. this.stack = new int[this.maxStack];

    }

    /**

    • 判断栈是否已满 *
    • @return true-满
    • false-未满 */ public boolean isFull() { return this.top == maxStack - 1; }

      /**

    • 判断栈是否为空栈 *
    • @return true-空栈
    • false-非空 */ public boolean isEmpty() { return this.top == -1; }

      /**

    • 入栈
    • 先移动栈顶指针,后赋值。
    • @param value-入栈值 */ public void push(int value) { //判断是否满栈 if (isFull()) {

      1. throw new RuntimeException("此栈已满!");

      } //若没满,栈顶指针上移 top++; //赋值 stack[top] = value; }

      /**

    • 弹栈
    • 先赋值,后移动栈顶指针。
    • @return */ public int pop() { //判断是否为空栈 if (isEmpty()) {

      1. throw new RuntimeException("此栈为空栈,无数据。");

      } //赋值 int value = stack[top]; //指针下移 top—;

      return value; }

      /**

    • 查看栈中全部数据 */ public void list(){ if (isEmpty()){
      1. throw new RuntimeException("此栈为空栈,无数据。");
      } for (int i = 0; i < stack.length; i++) {
      1. System.out.println("stack["+i+"]="+stack[i]+"\n");
      } } /**
    • 获取栈中元素存在的个数
    • 其实这个不太需要,我使用字符串的长度不就可以了么? */ public int length(){ return this.top++; } }
  1. <a name="HOElP"></a>
  2. ### 判断回文数
  3. **回文**:一个单词、短语或数字,从前往后写和从后往前写都是一样的。例如,dad,1001。<br />**思路**:使用栈,可以轻松判断一个字符串是否是回文。
  4. 1. 我们将拿到的字符串的每个字符按从左至右的顺序压入栈。
  5. 1. 当字符串中的字符都入栈后,栈内就保存了一个反转后的字符串。
  6. **代码:**
  7. ```java
  8. package ch01_stack.huiwenshu;
  9. import ch01_stack.ArrayStack;
  10. public class Test {
  11. public static void main(String[] args) {
  12. boolean b = detection("hello");
  13. System.out.println(b);
  14. boolean b1 = detection("1001");
  15. System.out.println(b1);
  16. }
  17. /**
  18. * 判断是否为回文数
  19. */
  20. public static boolean detection(String words) {
  21. ArrayStack arrayStack = new ArrayStack(10);
  22. //字符串长度
  23. int length = words.length();
  24. //将字符串压栈
  25. for (int i = 0; i < length; i++) {
  26. arrayStack.push(words.charAt(i));
  27. }
  28. //将字符弹栈
  29. String newVal = "";
  30. for (int i = 0; i < length; i++) {
  31. if (!arrayStack.isEmpty()) {
  32. char val = (char) arrayStack.pop();
  33. newVal = newVal + val;
  34. }
  35. }
  36. if (words.equals(newVal)){
  37. return true;
  38. }
  39. return false;
  40. }
  41. }

栈实现计算器

思路:
image.png
前备知识点总结:

  1. int→char可以自动类型转换
  2. charAt(int index):返回索引处字符。
  3. subString(int beginIndex, int endIndex):截取字符串,左闭右开。

代码:
数组模拟栈:

  1. package ch01_stack.calculator;
  2. /**
  3. * 使用数组模拟栈
  4. */
  5. public class ArrayStack {
  6. //栈的大小
  7. private int maxStack;
  8. //数组模拟栈
  9. private int[] stack;
  10. //表示栈顶,默认值为-1(空栈)。
  11. private int top = -1;
  12. //初始化栈的大小
  13. public ArrayStack(int maxStack) {
  14. this.maxStack = maxStack;
  15. //初始化数组容量
  16. this.stack = new int[this.maxStack];
  17. }
  18. /**
  19. * 判断栈是否已满
  20. *
  21. * @return true-满
  22. * false-未满
  23. */
  24. public boolean isFull() {
  25. return this.top == maxStack - 1;
  26. }
  27. /**
  28. * 判断栈是否为空栈
  29. *
  30. * @return true-空栈
  31. * false-非空
  32. */
  33. public boolean isEmpty() {
  34. return this.top == -1;
  35. }
  36. /**
  37. * 入栈
  38. * 先移动栈顶指针,后赋值。
  39. *
  40. * @param value-入栈值
  41. */
  42. public void push(int value) {
  43. //判断是否满栈
  44. if (isFull()) {
  45. throw new RuntimeException("此栈已满!");
  46. }
  47. //若没满,栈顶指针上移
  48. top++;
  49. //赋值
  50. stack[top] = value;
  51. }
  52. /**
  53. * 弹栈
  54. * 先赋值,后移动栈顶指针。
  55. *
  56. * @return
  57. */
  58. public int pop() {
  59. //判断是否为空栈
  60. if (isEmpty()) {
  61. throw new RuntimeException("此栈为空栈,无数据。");
  62. }
  63. //赋值
  64. int value = stack[top];
  65. //指针下移
  66. top--;
  67. return value;
  68. }
  69. /**
  70. * 查看栈中全部数据
  71. */
  72. public void list() {
  73. if (isEmpty()) {
  74. throw new RuntimeException("此栈为空栈,无数据。");
  75. }
  76. for (int i = 0; i < stack.length; i++) {
  77. System.out.println("stack[" + i + "]=" + stack[i] + "\n");
  78. }
  79. }
  80. /**
  81. * 获取栈中元素存在的个数
  82. * 其实这个不太需要,我使用字符串的长度不就可以了么?
  83. */
  84. public int length() {
  85. return this.top++;
  86. }
  87. /**
  88. * 查看栈顶元素
  89. *
  90. * @return-栈顶元素
  91. */
  92. public int peak() {
  93. return stack[top];
  94. }
  95. /**
  96. * 判断字符是否是运算符
  97. *
  98. * @param v:字符
  99. * @return:true-是运算符,false-不是运算符
  100. */
  101. public boolean isOper(char v) {
  102. return v == '+' || v == '-' || v == '*' || v == '/';
  103. }
  104. /**
  105. * 运算符优先级
  106. *
  107. * @param oper——运算符
  108. * @return——数字越大优先级越高
  109. */
  110. public int priority(char oper) {
  111. //乘除
  112. if (oper == '*' || oper == '/') {
  113. return 1;
  114. }
  115. //加减
  116. else if (oper == '+' || oper == '-') {
  117. return 0;
  118. }
  119. //其他
  120. else {
  121. return -1;
  122. }
  123. }
  124. /**
  125. * 计算功能
  126. *
  127. * @param num1:值1
  128. * @param num2:值2
  129. * @param oper:运算符
  130. * @return:计算结果
  131. */
  132. public int calculate(int num1, int num2, int oper) {
  133. int result = 0;
  134. switch (oper) {
  135. case '+':
  136. result = num1 + num2;
  137. break;
  138. case '-':
  139. result =num2-num1;
  140. break;
  141. case '*':
  142. result=num1*num2;
  143. break;
  144. case '/':
  145. result=num2/num1;
  146. break;
  147. default:
  148. break;
  149. }
  150. return result;
  151. }
  152. }

main方法:

  1. package ch01_stack.calculator;
  2. public class Test {
  3. public static void main(String[] args) {
  4. String str = "3*2+10";
  5. //数字栈
  6. ArrayStack numStack = new ArrayStack(10);
  7. //运算符栈
  8. ArrayStack symbolStack = new ArrayStack(10);
  9. //获取字符串长度
  10. int length = str.length();
  11. int temp1 = 0;
  12. int temp2 = 0;
  13. int symbolChar = 0;
  14. int result = 0;
  15. String values = "";
  16. for (int i = 0; i < length; i++) {
  17. //获取字符串中的每一个字符
  18. char c = str.charAt(i);
  19. //判断该字符是否是一个运算符,是
  20. if (symbolStack.isOper(c)) {
  21. /*
  22. 判断运算符栈是否为空栈
  23. true-非空
  24. false-空
  25. */
  26. if (!symbolStack.isEmpty()) {
  27. //比较当前运算符和栈中运算符的优先级
  28. /*
  29. 当前运算符优先级<=栈中运算符优先级
  30. 1.取出数字栈中的两个数字
  31. 2.取出运算符栈中的运算符
  32. 3.进行运算,求出结果
  33. 4.将结果再次放入数字栈中
  34. 5.将当前运算符放入运算符栈中
  35. */
  36. if (symbolStack.priority(c) <= symbolStack.priority((char) symbolStack.peak())) {
  37. //1.取出数字栈中的两个数字
  38. temp1 = numStack.pop();
  39. temp2 = numStack.pop();
  40. //2.取出运算符栈中的运算符
  41. symbolChar = symbolStack.pop();
  42. //3.进行运算
  43. result = numStack.calculate(temp1, temp2, symbolChar);
  44. //4.将结果再次放入数字栈中
  45. numStack.push(result);
  46. //5.将当前运算符放入运算符栈中
  47. symbolStack.push(c);
  48. }
  49. //当前运算符优先级 > 栈中运算符优先级
  50. else {
  51. //直接放入运算符栈中
  52. symbolStack.push(c);
  53. }
  54. }
  55. //运算符栈为空
  56. else {
  57. //直接放入运算符栈中
  58. symbolStack.push(c);
  59. }
  60. }
  61. /*
  62. 该字符不是运算符,则是数字。
  63. */
  64. else {
  65. //若是多位数字(33,44),则进行字符串拼接
  66. values += c;
  67. //若当前数字字符是最后一个字符,则直接放入数字栈中
  68. if (i == length - 1) {
  69. numStack.push(Integer.parseInt(values));
  70. }
  71. //若当前数字字符不是最后一个字符
  72. else {
  73. //截取下一位字符
  74. char data = str.substring(i + 1, i + 2).charAt(0);
  75. //判断该字符是否为运算符,若为运算符直接将数字压栈,并清空values。
  76. if (symbolStack.isOper(data)) {
  77. numStack.push(Integer.parseInt(values));
  78. values = "";
  79. }
  80. }
  81. }
  82. }
  83. //取出栈中数据进行计算
  84. while (true) {
  85. //若运算符栈为空,则跳出循环。
  86. if (symbolStack.isEmpty()) {
  87. break;
  88. }
  89. temp1 = numStack.pop();
  90. temp2 = numStack.pop();
  91. symbolChar = symbolStack.pop();
  92. result = numStack.calculate(temp1, temp2, symbolChar);
  93. //将运算结果压栈
  94. numStack.push(result);
  95. }
  96. //取出最终结果
  97. int res = numStack.pop();
  98. System.out.println("结果是:" + res);
  99. }
  100. }

链表

基本介绍

链表(Linked List)是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。
物理结构:
image.png
逻辑结构:
image.png
特点:

  1. 链表是以结点形式存储的,是链式存储。
  2. 每个结点都包含data区域(数据元素)和next区域(下一个结点的存储位置)。
  3. 各个结点并不是连续存储的
  4. 链表分为带头结点链表和无头结点链表。
  5. 链表的第一个结点被称为头结点,最后一个结点称为尾结点,尾结点的next指针指向空。

    链表的快速入门

    单链表的基本操作

    根据带有头部的单链表,实现商品增删改查,并且也可以针对商品的编号进行排序,完成排行榜。
    难点:

  6. temp==null 与 temp.next==null的区别?

temp.next==null 比 temp==null 早一步少了一次循环。
代码:
GoodsNode

  1. package ch02_linked;
  2. /**
  3. * 商品结点
  4. */
  5. public class GoodsNode {
  6. //商品id
  7. public int id;
  8. //商品名称
  9. public String name;
  10. //商品价格
  11. public double price;
  12. //指针,指向下一个结点
  13. public GoodsNode next;
  14. public GoodsNode(int id, String name, double price) {
  15. this.id = id;
  16. this.name = name;
  17. this.price = price;
  18. }
  19. @Override
  20. public String toString() {
  21. return "GoodsNode{" +
  22. "id=" + id +
  23. ", name='" + name + '\'' +
  24. ", price=" + price +
  25. '}';
  26. }
  27. }

DLLinkedList

  1. package ch02_linked;
  2. /**
  3. * 结点的增删改查
  4. */
  5. public class DLLinkedList {
  6. //头结点
  7. private GoodsNode node = new GoodsNode(0, "", 0.0);
  8. /**
  9. * 向链表中添加结点(尾端)
  10. *
  11. * @param goodsNode
  12. */
  13. public void add(GoodsNode goodsNode) {
  14. //将头结点赋值给临时变量
  15. GoodsNode temp = node;
  16. while (true) {
  17. //如果下一个结点为空,直接跳出循环
  18. if (temp.next == null) {
  19. break;
  20. }
  21. //如果下一个结点不为空,继续赋值
  22. temp = temp.next;
  23. }
  24. //将新增结点添加进去
  25. temp.next = goodsNode;
  26. }
  27. /**
  28. * 按照商品标号id进行添加结点,从小到大的顺序进行添加
  29. *
  30. * @param goodsNode
  31. */
  32. public void addByOrder(GoodsNode goodsNode) {
  33. GoodsNode temp = node;
  34. boolean flag = false;
  35. /*
  36. 跳出循环的3个条件:
  37. 1.下一个结点为空
  38. 2.当下一个结点的id值大于插入结点的id值时.
  39. 3.当下一个结点的id值等于插入结点的id值时。
  40. */
  41. while (true) {
  42. if (temp.next == null) {
  43. break;
  44. }
  45. if (temp.next.id > goodsNode.id) {
  46. break;
  47. } else if (temp.next.id == goodsNode.id) {
  48. flag = true;
  49. break;
  50. }
  51. temp = temp.next;
  52. }
  53. if (flag) {
  54. System.out.println("该商品id已存在,不能添加重复元素。");
  55. } else {
  56. //连接后面
  57. goodsNode.next = temp.next;
  58. //连接前面
  59. temp.next = goodsNode;
  60. }
  61. }
  62. /**
  63. * 修改结点
  64. *
  65. * @param goodsNode
  66. */
  67. public void updateNode(GoodsNode goodsNode) {
  68. //判断是否为空链表
  69. if (node.next == null) {
  70. System.out.println("链表为空!");
  71. return;
  72. }
  73. //将头结点赋值给临时变量
  74. GoodsNode temp = node;
  75. //用于区分跳出循环的两个条件
  76. boolean flag = false;
  77. /**
  78. *跳出循环的2个条件:
  79. * 1.在整个列表中未找到对应的结点
  80. * 2.找到了对应的结点
  81. */
  82. while (true) {
  83. /*
  84. temp与temp.next的区别?
  85. temp.next==null会比temp==null少循环一次。
  86. */
  87. if (temp == null) {
  88. break;
  89. }
  90. if (temp.id == goodsNode.id) {
  91. flag = true;
  92. break;
  93. }
  94. temp = temp.next;
  95. }
  96. if (flag) {
  97. temp.name = goodsNode.name;
  98. temp.price = goodsNode.price;
  99. } else {
  100. System.out.println("在整个列表中未找到对应的结点!");
  101. }
  102. }
  103. /**
  104. * 根据商品id删除结点
  105. *
  106. * @param id:商品id
  107. */
  108. public void delNode(int id) {
  109. //判断是否为空链表
  110. if (node.next == null) {
  111. System.out.println("链表为空!");
  112. return;
  113. }
  114. GoodsNode temp = node;
  115. boolean flag = false;
  116. /**
  117. *跳出循环的2个条件:
  118. * 1.在整个列表中未找到对应的结点
  119. * 2.找到了对应的结点
  120. */
  121. while (true) {
  122. if (temp.next == null) {
  123. break;
  124. }
  125. if (temp.next.id == id) {
  126. flag = true;
  127. break;
  128. }
  129. temp = temp.next;
  130. }
  131. if (flag) {
  132. //找到了对应结点,直接指向对应结点的下一个结点,等于删除。
  133. temp.next = temp.next.next;
  134. } else {
  135. System.out.println("在整个列表中未找到对应的结点!");
  136. }
  137. }
  138. /**
  139. * 查看链表中的每一个结点元素
  140. */
  141. public void list() {
  142. if (node.next == null) {
  143. System.out.println("链表为空!");
  144. return;
  145. }
  146. GoodsNode temp = node.next;
  147. while (true) {
  148. if (temp == null) {
  149. break;
  150. }
  151. System.out.println(temp);
  152. temp = temp.next;
  153. }
  154. }
  155. /**
  156. * 面试题:计算单链表中结点个数(不包括头结点)
  157. *
  158. * @return-结点个数
  159. */
  160. public int getLength() {
  161. if (node.next == null) {
  162. System.out.println("空链表");
  163. return 0;
  164. }
  165. GoodsNode temp = node.next;
  166. int length = 0;
  167. while (temp != null) {
  168. length++;
  169. temp = temp.next;
  170. }
  171. return length;
  172. }
  173. }

LinkedTest

  1. package ch02_linked;
  2. public class LinkedTest {
  3. public static void main(String[] args) {
  4. GoodsNode g1 = new GoodsNode(1,"a",99.0);
  5. GoodsNode g2 = new GoodsNode(2,"b",89.0);
  6. GoodsNode g3 = new GoodsNode(3,"c",79.0);
  7. DLLinkedList linkedList = new DLLinkedList();
  8. /* linkedList.add(g2);
  9. linkedList.add(g1);
  10. linkedList.add(g3);
  11. linkedList.list();*/
  12. linkedList.addByOrder(g2);
  13. linkedList.addByOrder(g1);
  14. linkedList.addByOrder(g3);
  15. System.out.println("------------");
  16. linkedList.list();
  17. linkedList.updateNode(new GoodsNode(3,"james",76.0));
  18. System.out.println("--------------------");
  19. linkedList.list();
  20. linkedList.delNode(3);
  21. System.out.println("--------------------");
  22. linkedList.list();
  23. int length = linkedList.getLength();
  24. System.out.println(length);
  25. }
  26. }

常见面试题

给定一个单链表,实现反转链表。
![85)J0VZI$67MII$2J_663T.png

双向链表

基本介绍

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
单向链表示意图:
(3{8FS~EMBQAV3Y0GGF3(`7.png
单向链表只知道自己的直接后继。

双向链表示意图:
VLA68`TE]}MV8U926S%QY_7.png
双向链表知道自己的直接前驱和直接后继。

双向链表的基本操作

根据带有头部的双向链表,实现图书的增删改查,并且也可以针对图书的编号进行排序,完成排行榜。
注:删除功能有bug
BookNode

  1. package ch02_linked.dual;
  2. public class BookNode {
  3. public int id;
  4. public String name;
  5. public double price;
  6. public BookNode pre;
  7. public BookNode next;
  8. public BookNode(int id, String name, double price) {
  9. this.id = id;
  10. this.name = name;
  11. this.price = price;
  12. }
  13. @Override
  14. public String toString() {
  15. return "BookNode{" +
  16. "id=" + id +
  17. ", name='" + name + '\'' +
  18. ", price=" + price +
  19. '}';
  20. }
  21. }

DualLinkedList

  1. package ch02_linked.dual;
  2. import ch02_linked.single.GoodsNode;
  3. import com.sun.imageio.plugins.wbmp.WBMPImageReader;
  4. /**
  5. * 双向链表结点的增删改查
  6. */
  7. public class DualLinkedList {
  8. //创建头结点
  9. private BookNode node = new BookNode(0, "", 0.0);
  10. /**
  11. * 向双向链表的末尾添加结点
  12. *
  13. * @param newNode
  14. */
  15. public void addLast(BookNode newNode) {
  16. //获取头结点
  17. BookNode temp = node;
  18. //寻找双向链表的末尾结点
  19. while (true) {
  20. if (temp.next == null) {
  21. break;
  22. }
  23. temp = temp.next;
  24. }
  25. /*
  26. 此时的temp就代表着链表的末尾结点
  27. temp的next指向新结点
  28. 新结点的pre指向temp
  29. 完成结点的添加
  30. */
  31. temp.next = newNode;
  32. newNode.pre = temp;
  33. }
  34. /**
  35. * 根据图书标号id进行添加结点,按照从小到大的顺序进行添加
  36. *
  37. * @param newNode
  38. */
  39. public void addByOrder(BookNode newNode) {
  40. BookNode temp = node;
  41. /**
  42. * 跳出循环的3个条件:
  43. * 1.当添加的结点为第一个结点时
  44. * 1.下一个结点为空
  45. * 2.当下一个结点的id值大于插入结点的id值时.
  46. * 3.当下一个结点的id值等于插入结点的id值时。
  47. */
  48. while (true) {
  49. //头部插入
  50. if (temp.next == null && temp.pre == null) {
  51. temp.next = newNode;
  52. newNode.pre = null;
  53. break;
  54. }
  55. //尾部插入
  56. if (temp.next == null && temp.pre != null) {
  57. temp.next = newNode;
  58. }
  59. //中间插入
  60. if (temp.id < newNode.id && temp.next.id > newNode.id) {
  61. // booksNode的后继指向temp的后一个"节点"
  62. newNode.next = temp.next;
  63. // temp的后一个"节点"的前驱指向booksNode
  64. temp.next.pre = newNode;
  65. // temp的后继指向"节点"booksNode
  66. temp.next = newNode;
  67. // booksNode的前期指向"节点"temp
  68. newNode.pre = temp;
  69. break;
  70. }
  71. if (temp.next.id == newNode.id) {
  72. System.out.println("该书籍已经有了" + temp.next.id);
  73. break;
  74. }
  75. temp = temp.next;
  76. }
  77. }
  78. /**
  79. * 修改结点内容
  80. *
  81. * @param newNode
  82. */
  83. public void updateNode(BookNode newNode) {
  84. //判断空链表
  85. if (node.next == null) {
  86. System.out.println("空链表..");
  87. return;
  88. }
  89. BookNode temp = node.next;
  90. boolean flag = false;
  91. /**
  92. * 跳出循环的条件:
  93. * 1.整个链表中未找到对应结点
  94. * 2.找到了对应结点
  95. */
  96. while (true) {
  97. if (temp.id == newNode.id) {
  98. flag = true;
  99. break;
  100. }
  101. if (temp.next == null) {
  102. break;
  103. }
  104. temp = temp.next;
  105. }
  106. if (flag) {
  107. temp.name = newNode.name;
  108. temp.price = newNode.price;
  109. } else {
  110. System.out.println("未找到对应结点!");
  111. }
  112. }
  113. /**
  114. * 根据id编号删除对象结点
  115. *
  116. * @param
  117. */
  118. public void delNode(BookNode bookNode) {
  119. if (node.next == null) {
  120. System.out.println("空链表..");
  121. return;
  122. }
  123. BookNode temp = node;
  124. /**
  125. * 跳出循环的条件:
  126. * 1.在整个链表中未找到对应结点
  127. * 2.找到了对应结点
  128. */
  129. while (true) {
  130. if (temp.next == null) {
  131. System.out.println("未找到对应的结点!");
  132. break;
  133. }
  134. if (temp.next.id == bookNode.id) {
  135. System.out.println("恭喜成功删除" + temp.next + "结点!");
  136. if (temp.next.next == null) {
  137. temp.next = null;
  138. break;
  139. }
  140. temp.next.next.pre = temp;
  141. temp.next = temp.next.next;
  142. break;
  143. }
  144. temp = temp.next;
  145. }
  146. }
  147. /**
  148. * 遍历链表
  149. */
  150. public void list() {
  151. if (node.next == null) {
  152. System.out.println("空链表!");
  153. return;
  154. }
  155. BookNode temp = node.next;
  156. while (true) {
  157. if (temp == null) {
  158. break;
  159. }
  160. System.out.println(temp);
  161. temp = temp.next;
  162. }
  163. }
  164. }

DualLinkedListTest

  1. package ch02_linked;
  2. import ch02_linked.dual.BookNode;
  3. import ch02_linked.dual.DualLinkedList;
  4. public class DualLinkedListTest {
  5. public static void main(String[] args) {
  6. BookNode b1 = new BookNode(1,"james",66.00);
  7. BookNode b2 = new BookNode(2,"kobe",66.00);
  8. BookNode b3 = new BookNode(3,"davis",66.00);
  9. BookNode b4 = new BookNode(4,"paul",66.00);
  10. DualLinkedList dualLinkedList = new DualLinkedList();
  11. //测试添加
  12. dualLinkedList.addLast(b1);
  13. dualLinkedList.addLast(b3);
  14. dualLinkedList.addByOrder(b2);
  15. dualLinkedList.addByOrder(b4);
  16. dualLinkedList.list();
  17. System.out.println("-----------------");
  18. /*
  19. 测试删除
  20. 有bug:只能删除第一个结点,后面的结点的删除都多少有点bug
  21. */
  22. dualLinkedList.delNode(1);
  23. dualLinkedList.list();
  24. System.out.println("--------------");
  25. //测试修改
  26. dualLinkedList.updateNode(new BookNode(3,"myt",66.00));
  27. dualLinkedList.list();
  28. }
  29. }

单向环形链表(不理解)

基本介绍

image.png
如果把单链表的最后一个节点的指针指向链表头部,而不是指向NULL,那么就构成了一个单向循环链表,通俗讲就是把尾节点的下一跳指向头结点。

约瑟夫问题(约瑟夫环)

image.png
解决思路(思路
image.png

  1. 做环形链表
  2. 解决约瑟夫问题

代码: