当前位置:   article > 正文

【数据结构篇】线性表1 --- 顺序表、链表 (万字详解!!)_线性表和链表

线性表和链表

目录

 顺序表(ArrayList)

什么是顺序表? 

代码实现 (MyArrayList)

--- 打印顺序表 

--- 新增元素 

1.新增元素,默认在数组最后新增

2.在指定位置新增元素 

--- 判断是否包含某个元素

--- 查找某个元素具体位置(下标)

--- 获取 pos 位置的元素

--- 给pos位置 的值设为 value 

--- 获取顺序表长度

--- 删除第一次出现的关键字 

--- 清除顺序表 

完整代码

ArrayList

ArrayList 的实例化

ArrayList的构造 

ArrayList常见操作

ArrayList的遍历

---- 迭代器 

--- 用法一 

--- 用法二 

--- 从后往前打印 

----  for循环+下标

---- foreach 

ArrayList 的优缺点 

优点 

缺点 

链表 (LinkedList)

什么是链表?

单向 不带头 非循环 链表 

代码实现 

节点

插入数据 

--- 头插法 

 --- 尾插法

--- 任意位置插入(第一个数据节点为0号下标)

打印链表 

 单链表的长度

查找单链表中是否包含关键字key

删除第一次出现关键字为key的节点

删除所有值为key的节点

清除链表 

完整代码 

 双向 不带头 非循环 链表

代码实现 

创建节点内部类 

插入数据

--- 头插法 

--- 尾插法 

打印链表

查找链表中是否包含关键字key 

 链表的长度

任意位置插入,第一个数据节点为0号下标

删除第一次出现关键字为key的节点

删除所有值为key的节点

清除链表 

完整代码

 LinkedList的使用

LinkedList 的 常见方法 

LinkedList 的总结 

ArrayList和LinkedList的区别(顺序表和链表的区别)(面试题)


前言:这篇博客我们重点讲 线性表中的顺序表、链表

线性表(linear list)是n个具有相同特性的数据元素的有限序列

线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列...

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。 

 顺序表(ArrayList)

什么是顺序表? 

顺序表是用一段物理地址连续的存储单元,依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

 也就是说,顺序表  底层结构就是 一个数组。是使用数组 来完成的一种结构。

那现在有一个数组,如果有一组数据 1,2,3,4,5,6,7 , 我们现在想把这组数据放到数组里去。

我们先放进1,2,3 , 问当前数组里有多少个有效数据? 很明显是 3 个。

那怎么利用程序判断他有 3 个有效数据呢?

我们需要先定义一个变量 usedSize , 放进一个数据 usedSize++,放一个就加一下。这不就能确定有几个有效数据了吗。

代码实现 (MyArrayList)

接下来我们利用代码来实现顺序表的增删查改等方法: 

--- 打印顺序表 

就是遍历数组 

--- 新增元素 

1.新增元素,默认在数组最后新增

 这么写没问题吗?如果满了怎么办?

所有我们要写一个方法来判断是否满了 :

新增元素前我们要判断是否满了,满了的话要扩容 

新增看看效果:

  

2.在指定位置新增元素 

我们的逻辑应该是把指定位置之后的数据 都向后,把指定位置空出来,再在指定位置插入数据 。

那具体该如何挪数据? 

我们要从最后的位置开始挪 ,不能从第一个开始挪,不然会把之后的数据盖掉。

 

那我们现在确定了挪的方法,接下来我们看看代码如何实现: 

注意:我们这里是要判断pos的位置是否合法的 ,

pos不能小于0,也不能跳着放(大于usedSize).

为此我们还搞了个异常 

(ps:异常之前讲过 链接 https://blog.csdn.net/iiiiiihuang/article/details/130671801?spm=1001.2014.3001.5501 )

我们看看运行效果 :

看看位置不合法时的运行结果: 

--- 判断是否包含某个元素

运行结果  


--- 查找某个元素具体位置(下标)

运行结果 

--- 获取 pos 位置的元素

还要判断位置是否合法。

运行结果 

--- 给pos位置 的值设为 value 

 这里同样要先判断 pos 位置是否合法,那我们可以单独写一个方法,来判断。(方法的封装)

运行结果 

--- 获取顺序表长度

--- 删除第一次出现的关键字 

代码实现 

 

注意:usedSize - 1 防止越界,这里是 this.elem[i] = this.elem[i + 1], 那 i 走到倒数第二位就行了

运行结果 

--- 清除顺序表 

 

ps (小提一嘴): 现在的都是整型这么写可以,但是的是 引用类型 的时候要一个一个 置为 null ,删除也要有类似操作。

完整代码

  1. import java.util.ArrayList;
  2. import java.util.Arrays;
  3. /**
  4. * @Author: iiiiiihuang
  5. */
  6. public class MyArrayList {
  7. private int[] elem;//存放数据元素。
  8. private int usedSize;//代表当前顺序表中有效数据个数。
  9. private static final int DEFAULT_SIZE = 10;
  10. /**
  11. * 默认构造方法
  12. */
  13. public MyArrayList() {
  14. this.elem = new int[DEFAULT_SIZE];
  15. }
  16. /**
  17. * 指定容量
  18. * @param initCapacity
  19. */
  20. public MyArrayList(int initCapacity) {
  21. this.elem = new int[initCapacity];
  22. }
  23. /**
  24. * 打印顺序表中的所有元素
  25. */
  26. public void display() {
  27. for (int i = 0; i < this.usedSize; i++) {
  28. System.out.print(this.elem[i] + " ");
  29. }
  30. }
  31. public boolean isFull() {
  32. if(this.usedSize == this.elem.length){
  33. return true;
  34. }
  35. return false;
  36. }
  37. /**
  38. * 新增元素,默认在数组最后新增
  39. * @param date
  40. */
  41. public void add(int date){
  42. if(isFull()){
  43. //扩容
  44. this.elem = Arrays.copyOf(this.elem, 2*this.elem.length);
  45. }
  46. this.elem[this.usedSize] = date;
  47. this.usedSize++;
  48. }
  49. //判断pos位置是否合法
  50. private void checkPos(int pos){
  51. if(pos < 0 || pos >= usedSize){
  52. throw new PosOutOfBoundsException(pos + "位置不合法");
  53. }
  54. }
  55. /**
  56. * 在指定位置(pos)新增元素
  57. * @param pos
  58. * @param date
  59. */
  60. public void add(int pos, int date) {
  61. //判断pos位置是否合法
  62. if(pos < 0 || pos > usedSize){
  63. throw new PosOutOfBoundsException(pos + "位置不合法");
  64. }
  65. if(isFull()){
  66. this.elem = Arrays.copyOf(this.elem, 2*this.elem.length);
  67. }
  68. for (int i = this.usedSize - 1; i >= pos; i--) {
  69. this.elem[i + 1] = this.elem[i];
  70. }
  71. this.elem[pos] = date;
  72. usedSize++;
  73. }
  74. /**
  75. * 判断是否包含某个元素
  76. * @param toFind
  77. * @return
  78. */
  79. public boolean contains(int toFind){
  80. for (int i = 0; i < this.usedSize; i++) {
  81. if(this.elem[i] == toFind){
  82. return true;
  83. }
  84. }
  85. return false;
  86. }
  87. /**
  88. * 查找某个元素具体位置(下标)
  89. * @param toFind
  90. * @return
  91. */
  92. public int indexOf(int toFind){
  93. for (int i = 0; i < this.usedSize; i++) {
  94. if(this.elem[i] == toFind){
  95. return i;
  96. }
  97. }
  98. return -1;
  99. }
  100. /**
  101. * 获取 pos 位置的元素
  102. * @param pos
  103. * @return
  104. */
  105. public int get(int pos) {
  106. //判断pos位置是否合法
  107. checkPos(pos);
  108. return this.elem[pos];
  109. }
  110. /**
  111. * 给pos位置 的值设为 value
  112. * @param pos
  113. * @param value
  114. */
  115. public void set(int pos, int value) {
  116. checkPos(pos);
  117. this.elem[pos] = value;
  118. }
  119. /**
  120. * 获取顺序表长度
  121. */
  122. public int size() {
  123. return this.usedSize;
  124. }
  125. /**
  126. * 删除第一次出现的关键字
  127. * @param toRemove
  128. */
  129. public void remove(int toRemove) {
  130. //获取要删除元素下标
  131. int index = indexOf(toRemove);
  132. if(index == -1){
  133. System.out.println("没有这个数据");
  134. return;
  135. }
  136. //usedSize - 1 防止越界
  137. for (int i = index; i < this.usedSize - 1; i++) {
  138. this.elem[i] = this.elem[i + 1];
  139. }
  140. usedSize--;
  141. }
  142. /**
  143. * 清除顺序表
  144. */
  145. public void clear() {
  146. this.usedSize = 0;
  147. }
  148. }

上述是自己实现一个顺序表结构,那以后用到顺序表都要我们自己重新实现吗? 当然不用啦! 

Java里面已经帮你处理好了,有现成的 ,就是 ArrayList

ArrayList
 

在集合框架中,ArrayList是一个普通的类,实现了List接口 。 

1. ArrayList是以泛型方式实现的,使用时必须要先实例化
2. ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
3. ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
4. ArrayList实现了Serializable接口,表明ArrayList是支持序列化
5. 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择          Vector或者CopyOnWriteArrayList
6. ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表 

我们可以在 IDEA 里看到 ArrayList 的源码

接下来我们 来介绍一下 ArrayList 的几种用法。

ArrayList 的实例化

 这两种方法都行,区别就在于,方法一 可以调用的方法更多一些。 但是方法二 发生了向上转型,

一般情况下,我们用方法二多一点。

(ps: 向上转型 前面介绍过了 链接  https://blog.csdn.net/iiiiiihuang/article/details/130484383?spm=1001.2014.3001.5501 )

ArrayList的构造 

方法解释
ArrayList()无参构造
ArrayList(Collection<? extends E> c)利用其他 Collection 构建 ArrayList
ArrayList(int initialCapacity)指定顺序表初始容量

当我们调用不带参数的构造方法时,默认在第一次 add 时才会分配大小为10的内存

扩容按1.5倍进行扩容 

  

ArrayList常见操作
 

方法解释
boolean add(E e)尾插 e
void add(int index, E element)将 e 插入到 index 位置
boolean addAll(Collection<? extends E> 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<E> subList(int fromIndex, int toIndex)截取部分 list

注意:这个remove怎么用? 

 

要这么写才能删掉 等于2 的元素 

演示一下 subList

截取到2,3    (Java里一般都是左闭右开的 [ , , )   ) 

如果我们把 list3 的 0 下标该成 188 会方生什么?

我们发现,list2 的 1 下标的位置(list3 的 0下标)也变成了188  . 为什么? 

这是因为list3, 截取的时候并没有真正的把这个数据拿出来,只是指向了1 下标那儿的地址 ,所有更新值肯定会影响 list2.

ArrayList的遍历
 

ArrayList 可以使用三方方式遍历:使用迭代器 ,for循环+下标、foreach 

 上面是直接 用sout 遍历 的,是因为重写了 toString 方法。 

---- 迭代器 

一般情况下,能够直接通过 sout 输出 引用指向对象当中的内容的时候,此时一定重写了 toString 方法。 

那我们看看ArrayList 里有没有 toString (ctrl + F) 搜索一下。发现没有。

 

但是 ArrayList  还继承了 AbstractList,我们去 AbstractList 里找找。发现还没有。

但是 AbstractList 还继承了 AbstractCollection ,

我们在 AbstractCollection 这里找到了 toString。

下面画线那个部分就是 迭代器 (遍历当前集合)

--- 用法一 

--- 用法二 

上面两个用那个都行 

--- 从后往前打印 

ps : 这个方法不行,因为这个不能传参 

----  for循环+下标

---- foreach 

 

 

ArrayList 的优缺点 

优点 

 1.可以通过下标 进行随机访问,顺序表适合对静态的数据进行 查找 和 更新

缺点 

1.添加元素的效率比较低 (假如在 0 位置添加元素,就需要把后面所有的元素都往后移)

2.删除的效率也低(假如删除 0 位置元素,也需要移动后面所有的元素)

3.扩容 按1.5 倍扩容,有浪费 空间的情况。

(顺序表不适合用来 插入和删除 数据)

顺序表适合对静态的数据进行 查找 和 更新,不适合用来 插入和删除 数据,这时候就需要用 链表来做。接下来我们来介绍链表。

链表 (LinkedList)

什么是链表?
 

链表 是一种 物理存储结构上非连续 的存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接次序实现的 。

链表是由一个一个 节点 连成的 :(这个是单向 不带头 非循环 链表的节点)

 

单向 不带头 非循环 链表 

除此之外还有很多 类型的 链表,一般从三个方向去分 

  • 单向    双向
  • 不带头   带头
  • 非循环   循环  

 经过排列组合就会有下面这几种类型的链表:

  • 单向 不带头 非循环 链表
  • 单向 不带头 循环 链表
  • 单向 带头 非循环 链表
  • 单向 带头 循环 链表
  • 双向 不带头 非循环 链表
  • 双向 不带头 循环 链表
  • 双向 带头 非循环 链表
  • 双向 带头 循环 链表

上面我们画了不带头的 链表,接下来我们了解一下 带头的是啥样的?

再来看看 循环的

等我们把单向的介绍完,在介绍双向的 。

我们重点讲 单向 不带头 非循环 链表 和 双向 不带头 非循环 链表 (这两种是工作,笔试面试考试重点)

单向 不带头 非循环 链表 

代码实现 

节点

上述可知 链表是由一个一个 节点 组成的,所以我们可以把这个节点定义成个内部类 。

还得有一个 指向 第一个节点 的属性:

插入数据 
--- 头插法 

  

注意:如果我们把下面的语句反过来写可以吗? 变成 head = node;  node.next = head;

 

 不可以,先把node 置为 head, node 的下一个指向 head, 那不就是 自己指向自己了吗,后面的节点就都丢失了。

我们插入看看效果 

 --- 尾插法

先找到最后一个节点,

所以循环里要写成 cur.next != null, 在到最后一个节点这就不进入循环了,那此时cur 就是最后一个节点,而不能写成cur != null, 这样会遍历完整个链表,停不住。 

 

运行看看效果: 

似乎是可以的,但是如果现在链表了一个节点也没有,再插入呢?

报错了!!—— 空指针异常 ,为啥呀? 我们返回代码哪里看一看,调试一下:

我们发现当 链表里一个节点都没有的时候,head  = null,那cur = head, cur 也是 null,

cur都为空了,哪来的 next ,那当然就报出来空指针异常了。

所以接下来我们改进一下代码: 

那我们再运行一下代码,就没有任何问题了

--- 任意位置插入(第一个数据节点为0号下标)
 

很明显,我们要确定 插入位置的前一个节点, 那如果你要插入 2 位置,那你就要找到 1 位置节点,那从 0 位置到 1 位置要走一步,也就是 index - 1 步。

找到要插入位置的前一个节点。这个是写方法 里面,还是单独封装看你喜好。

(我用的 while 循环,如果用for 循环的话,i < index - 1,  我觉得while 循环好理解点。)

 

 找到位置了,我们就可以插入了。 

先判断插入位置合不合法(类比上面的顺序表的部分)

都要先和后边的节点建立联系哦  

看看效果

打印链表 

 

但是用上面的方式走完之后,我自己都不知道头节点在哪里了 ,很可怕耶,

所以我们定义一个 cur节点 来代替 头节点 往下走。

 

 单链表的长度

 

查找单链表中是否包含关键字key

删除第一次出现关键字为key的节点
 

还是要找到删除节点的前一个,

为什么,循环那里要 cur.next != null, 因为,cur.next 不能是null,因为如果它是 null 的话,

那cur.next.val 就找不到值,那就会引起空指针异常,所以只要走到倒数第一个停住就好了(cur走到最后一个节点时不进入循环) 

看看效果:

删除所有值为key的节点
 

注意 :我们删除的是 cur 对应的值,cur 从第二个节点开始走,那如果第一个也是23(要删除的值)呢 ,所以我们要单独删除头节点,且在最后,不能放在前面。

运行结果 

清除链表 

完整代码 

  1. /**
  2. * @Author: iiiiiihuang
  3. */
  4. public class MySingleLinkedList {
  5. //把节点定义成个内部类
  6. static class ListNode {
  7. public int val;//节点的值域
  8. public ListNode next;//下一个节点的地址
  9. public ListNode(int val) {
  10. this.val = val;
  11. }
  12. }
  13. public ListNode head;//头节点
  14. /**
  15. * 头插法
  16. * @param data
  17. */
  18. public void addFirst(int data) {
  19. //给插入的数据创个节点
  20. ListNode node = new ListNode(data);
  21. node.next = head;
  22. head = node;
  23. }
  24. /**
  25. * 尾插法
  26. * @param data
  27. */
  28. public void addLast(int data) {
  29. ListNode node = new ListNode(data);
  30. ListNode cur = head;
  31. if(cur == null) {
  32. head = node;
  33. return;
  34. }
  35. //先找到最后一个节点
  36. while (cur.next != null) {
  37. cur = cur.next;
  38. }
  39. cur.next = node;
  40. }
  41. /**
  42. * 打印链表
  43. */
  44. public void display() {
  45. ListNode cur = head;
  46. while(cur != null) {
  47. System.out.print(cur.val + " ");
  48. cur = cur.next;
  49. }
  50. System.out.println();
  51. }
  52. /**
  53. * 任意位置插入
  54. * @param index
  55. * @param data
  56. */
  57. public void addIndex(int index,int data) {
  58. ListNode node = new ListNode(data);
  59. //先判断插入位置合不合法
  60. if(index < 0 || index > size()){
  61. throw new IndexOutOfBoundsException();
  62. }
  63. if(index == 0) {
  64. addFirst(data);
  65. return;
  66. }
  67. if(index == size()) {
  68. addLast(data);
  69. return;
  70. }
  71. ListNode cur = findIndexSubOne(index);
  72. node.next = cur.next;
  73. cur.next = node;
  74. }
  75. //找到要插入位置的前一个节点
  76. private ListNode findIndexSubOne(int index) {
  77. ListNode cur = head;
  78. while (index - 1 != 0) {
  79. cur = cur.next;
  80. index--;
  81. }
  82. return cur;
  83. }
  84. /**
  85. * 单链表的长度
  86. * @return
  87. */
  88. public int size() {
  89. ListNode cur = head;
  90. int count = 0;
  91. while (cur != null) {
  92. count++;
  93. cur = cur.next;
  94. }
  95. return count;
  96. }
  97. /**
  98. * 查找单链表中是否包含关键字key
  99. * @param key
  100. * @return
  101. */
  102. public boolean contains(int key) {
  103. ListNode cur = head;
  104. while (cur != null) {
  105. if(cur.val == key) {
  106. return true;
  107. }
  108. cur = cur.next;
  109. }
  110. return false;
  111. }
  112. /**
  113. * 删除第一次出现关键字为key的节点
  114. * @param key
  115. */
  116. public void remove(int key) {
  117. if(head == null) {
  118. return;
  119. }
  120. //单独删除头节点
  121. if(head.val == key) {
  122. head = head.next;
  123. return;
  124. }
  125. //找到删除节点的前一个
  126. ListNode cur = findRemSubOne(key);
  127. if(cur == null) {
  128. System.out.println("没有你要删除的数字");
  129. return;
  130. }
  131. ListNode rem = cur.next;
  132. cur.next = rem.next;
  133. }
  134. //找到删除节点的前一个节点
  135. private ListNode findRemSubOne(int key) {
  136. ListNode cur = head;
  137. //这里跳过了头节点
  138. while (cur.next != null) {
  139. if (cur.next.val == key) {
  140. return cur;
  141. }
  142. cur = cur.next;
  143. }
  144. return null;
  145. }
  146. /**
  147. * 删除所有值为key的节点
  148. * @param key
  149. */
  150. public void removeAllKey(int key) {
  151. ListNode cur = head.next;
  152. ListNode prev = head;
  153. while (cur != null) {
  154. if(cur.val == key) {
  155. prev.next = cur.next;
  156. } else {
  157. prev = cur;
  158. }
  159. cur = cur.next;
  160. }
  161. //删除头节点
  162. if(head.val == key) {
  163. head = head.next;
  164. }
  165. }
  166. /**
  167. * 清除链表
  168. */
  169. public void clear() {
  170. this.head = null;
  171. }
  172. }

 双向 不带头 非循环 链表

画图看看结构

代码实现 

创建节点内部类 

上面有的方法这也有

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/633370
推荐阅读
相关标签