我们之前已经学习了三种线性数据结构。他们的底层依然是“依托静态数组”,靠resize解决固定容量问题。
动态是相对用户来说的。但是链表是真正的动态数据结构。
class Node{
E e;
Node next;
}
(1)当前节点存储两个内容:
(2)例如
一个链表就像一节火车,每一个节点就像一节车厢,我们在车厢内存储真正的数据。车厢之间要进行连接,使得我们的数据是整合在一起的,用户可以方便在所有的数据中进行操作。数据的连接使用next完成的。
链表存储的数据是有限的,最后一个节点存储的next就是NULL。所以如果一个节点的next是NULL,这就说明它存储的是最后一个节点。
比如一个火车有10节车厢,车厢头是 "1",车厢尾是 "10"。
对于车厢"8"来说,它存储的两个内容是:
当前值“章”,它的下一个车厢 "9",其中车厢“9”中包含了车厢“10”.
对于车厢"1"来说,它存储的两个内容是:
当前值“填”,它的下一个车厢 "2",其中车厢“2”包含了后面所有的车厢。
1 ---> 2 ---> 3 ---> 4 ---> 5 ---> 6 ---> 7 --> 8 ---> 9 ---> 10 --> NULL
天 子 重 英 豪 文 章 教 尔 曹
对于链表来说,你需要多少个数据,就可以生成多少个节点,把他们挂接起来。
相比较数组来说,数组可以给个索引,直接从数组中拿到对应的值。这是因为从底层机制来说,数组开辟的空间从内存上来说,是连续分布的,所以我们可以直接寻找这个索引对应的偏移,直接计算出相应的数据所存储的内存地址。
但是链表不同,由于链表是靠next一层一层连接的,所以在计算机的底层,每一个节点所在的内存的位置是不同的。我们必须依靠next一点一点找到我们想要找的元素。
public class LinkdeList {
//使用内部类来表示节点
private class Node{
public E e;
public Node next;
public Node(E e, Node next){
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString(){
return e.toString();
}
}
private Node haed;
private int size;
public LinkdeList(){
head = null;
size = 0;
}
//获取链表中的元素个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return size==0;
}
}
需求: 如果我们想将“666”这个节点添加入链表头。
解决:
(1)将“666”的next指向head. 然后让head指向“666”节点
(2)这样,节点“666”就被插入到了这个链表的头部。由于这个过程在函数中执行,对于node变量,在函数结束之后,它的快作用域就结束了,这个变量就没用了。
public class LinkdeList {
private Node head;
private int size;
public LinkdeList(){
head = null;
size = 0;
}
//为链表头添加元素(在链表头添加元素很方便,是因为我们有个变量head来跟踪链表头)
public void addFist(E e){
//写法一
Node node = new Node(e);
node.next = head;
head = node;
//写法二
head = new Node(e, head);
size++;
}
}
需求: 在索引为2的地方添加元素666
解决:
(1) 知道要添加的节点“666”之前的节点,称为prev节点。这里也就是索引为1的节点。
(2) 找到prev之后,将“666”.next指向pre.next。也就是: node.next = prev.next.
(3) 然后将prve.next指向“666”.也就是 prev.next = node。
【注意:如果先执行prev.next = node,此时prex.next已经等于node.next了,后面的条件node.next = prev.next就不成立了】
(4) 然后就完成了添加过程。
(5) 这个过程的关键:找到要添加的节点的前一个节点。
(6) 但是对于添加链表的头部而言,头部是没有前一个节点的,所以对此要特殊处理
//为链表的Index位置添加元素(Index位置从0开始记)。但是这个操作不是一个常用的操作,仅作练习
public void add(int index, E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
if(index == 0){
addFist(e);
}else{
//1.找到待插入节点的前一个节点
Node prev = head;
for(int i=0; i
需求: 在我们上一个中,我们写了向链表中间添加元素的代码。但是当时我们对在【链表的开头添加元素】做了特殊处理。究其原因,是我们的上一节中在向链表添加元素的时候,要找到【待添加位置相应之前的节点】,但是对于在链表头添加元素来说没有之前的节点。所以我们就需要另外一种方法来代替上一节的操作: 【为链表设立虚拟头节点】。
解决:
(1)我们在链表头之前给一个节点,这个节点不存储任何元素,所以值写为null. 我们叫它dummyHead. 对于这个节点,实际是不存在的,对于用户来说看不到也没有意义,是我们自己虚构的节点。
(2)因为定义了dummyHead,那么以前定义的【head】变量就可以被代替,那么就将之前的代码变为:
public class LinkdeList {
private Node dummyHead;
private int size;
public LinkdeList(){
dummyHead = new Node(null,null);
size = 0;
}
}
(3)然后逻辑是一样的,我们需要找到这个节点的前一个位置的节点。
1. 对于上一个add()方法,我们找到前一个位置节点的代码如下.这里的for循环中的判断条件是【i
Node prev = haed;
//1.找到待插入节点的前一个节点
for(int i=0; i
2. 但是对于虚拟头节点来说,它前面相当于多了一个节点,这里的循环条件就要变化:
Node prev = dummyHead;
//1.找到待插入节点的前一个节点
for(int i=0; i
public class LinkdeList {
private Node dummyHead;
private int size;
public LinkdeList(){
dummyHead = new Node(null,null);
size = 0;
}
//使用虚拟头节点添加元素
public void add(int index, E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node prev = dummyHead;
//1.找到待插入节点的前一个节点
for(int i=0; i
需求: 获得index位置的节点的值
//获得链表的第index(0-based)个位置的元素
public E get(int index) {
//1. 对合法性进行判断
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
//2.遍历链表--这里是找到index位置的元素,也就是当前元素
Node curr = dummyHead.next;
for(int i=0; i< index; i++){
curr = curr.next;
}
return curr.e;
}
需求: 修改第index位置的元素
//修改链表的第index(0-based)个位置的元素
public void set(int index, E e) {
//1. 对合法性进行判断
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
//2.遍历链表--这里是找到index位置的元素,也就是当前元素
Node curr = dummyHead.next;
for(int i=0; i< index; i++){
curr = curr.next;
}
curr.e = e;
}
//查找链表中是否有元素e
public boolean contians(E e) {
Node curr = dummyHead.next;
while(curr != null){
if(curr.e.equals(e)){
return true;
}
curr = curr.next;
}
return false;
}
1.写一个toString()来方便打印
public class LinkdeList {
@Override
public String toString(){
StringBuilder res = new StringBuilder();
Node curr = dummyHead.next;
//循环写法一
while(curr != null){
res.append(curr + "->");
curr = curr.next;
}
//循环写法二
// for(Node cur = dummyHead.next; curr!=null; curr=curr.next){
// res.append(curr + "->");
// curr = curr.next;
// }
res.append("NULL");
return res.toString();
}
}
2. 写测试代码
public static void main(String[] args){
LinkdeList linkdedList = new LinkdeList<>();
for(int i=0; i<5; i++){
linkdedList.addFist(i);
}
linkdedList.add(2,666);
System.out.println(linkdedList);
}
3.测试结果
4->3->666->2->1->0->NULL
需求: 使用用虚拟头节点的链表来删除索引为2位置的元素
解决:
步骤一:对于删除元素,我们依然要找到链表的头一个位置的节点。
步骤二:我们要做的就是: prev.next = delNode.next; prev节点跳过了原本的delNode节点,指向了原本的delNode.next;把2位置的节点跳过去了。这里我的理解是这样的:【prev中由两个属性,其中e保存的是值,next保存的是下一个的地址。我们这里的prev.next = delNode.next改变的就是prev的下一个地址的指向,所以可以改变链表的结构。】(仅供参考)
步骤三:为了让java能够回收空间,我们应该手动的让2位置的节点的next和链表脱离。也就是让delNode.next指向一个NULL;这个时候才真正的把2位置的节点从链表中删除了。
【问题1】:一些人会觉得我既然我是删除delNode,那我直接令delNode=null,不就好了。
【解答1】:这里的delNode是一个临时引用类型变量,它是内存实际存储值的引用,你令delNode=null,只是改变了delNode这个引用的指向,等于它不再指向222了。但是222本身和333之间的联系仍然存在,也就是待删除节点的next仍然指向原先他所指向的位置。
【问题2】:如果我们令delNode.next=null.那delNode不用手动清除吗?
【解答2】:在delNode和333脱离关系后,我们不用手动给delNode设置为空,因为它本身就是这个函数种创建的临时变量,函数声明周期结束之后,这个变量的生命周期也结束了。至于他所指向的空间,由于不再和其他内存空间有任何联系,会由java的垃圾回收自动处理。
步骤四:疑惑问题
关于链表的删除,有一部分意见认为可以这样:找到待删除位置的元素delNode,标记为delNode。这个时候,只要让 : delNode =delNode.next ,这个时候就会delNode这个元素就会被删除。这个想法是错误的。就如如下做法:
1) Node delNode= prev.delNode;
2) delNode= delNode.next;
【我的理解(仅供参考)】
1. 这两句话句话做的操作是:创建一个临时的引用变量delNode,这个引用变量原来存储的是222的内存地址,但是经过第二句代码之后,现在delNode的引用变量指向delNode.next所在的内存空间。但是自始至终都是delNode这个临时变量在变化,只是将dleNode的指向位置从原来的一个位置变到了另外一个位置。和我们链表本身没有任何关系。这个涉及到引用数据类型的概念,可以参考我另外一篇文章【java基础【一】基本类型变量和引用类型变量】。
2. 对于delNode=delNode.next这句话,我们可以这样理解:比如链表的toString中,循环里就一直在使用cur = cur.next的方式,从头到尾遍历我们的整个链表,每次调用cur = cur.next,cur就像后移动了一个节点。
3. 如果我们想让整个链表发生改变,我们必须把链表中的节点中相应的next指向发生变化。所以prev这个节点怎么变化并不重要,重要的是我们必须要prev这个节点指正确的位置。
步骤五:实践delNode= delNode.next
举个例子,现在有一个链表 ,在2位置设置为cur,值是222。
1) 如果代码是cur= cur.next.在执行执行代码前面,我们已经遍历到了2位置,目前的2位置的元素的值是222.它的下一个节点,也就是curr.next的值,是333,再下一个,也就是curr.next.next的值,是444。
2) 如果在代码cur= cur.next之后. 查看链表,本身没有任何改变。只是改变了curr的值。
2. 代码
//从链表中删除index(0-based)位置的元素,返回删除的元素
public E remove(int index){
if(index<0 || index>=size) {
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
//1.找到待删除节点的之前的一个节点
Node prev = dummyHead;
for(int i=0; i
public static void main(String[] args){
LinkdeList linkdedList = new LinkdeList<>();
linkdedList.addFist(000);
linkdedList.add(1,111);
linkdedList.add(2,222);
linkdedList.add(3,333);
linkdedList.add(4,444);
System.out.println(linkdedList);
linkdedList.remove(2);
System.out.println(linkdedList);
}
测试结果
0->111->222->333->444->NULL
0->111->333->444->NULL
public class LinkdeList {
//使用内部类来表示节点
private class Node{
public E e;
public Node next;
public Node(E e, Node next){
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString(){
return e.toString();
}
}
private Node dummyHead;
private int size;
public LinkdeList(){
dummyHead = new Node(null,null);
size = 0;
}
//获取链表中的元素个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return size==0;
}
//为链表末尾添加元素
public void addLast(E e){
add(size, e);
}
//使用虚拟头节点添加元素
public void add(int index, E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node prev = dummyHead;
//1.找到待插入节点的前一个节点
for(int i=0; i size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
//2.遍历链表--这里是找到index位置的元素,也就是当前元素
Node curr = dummyHead.next;
for(int i=0; i< index; i++){
curr = curr.next;
}
return curr.e;
}
//修改链表的第index(0-based)个位置的元素
public void set(int index, E e) {
//1. 对合法性进行判断
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
//2.遍历链表--这里是找到index位置的元素,也就是当前元素
Node curr = dummyHead.next;
for(int i=0; i< index; i++){
curr = curr.next;
}
curr.e = e;
}
//查找链表中是否有元素e
public boolean contians(E e) {
Node curr = dummyHead.next;
while(curr != null){
if(curr.e.equals(e)){
return true;
}
curr = curr.next;
}
return false;
}
//从链表中删除index(0-based)位置的元素,返回删除的元素
public E remove(int index){
if(index<0 || index>=size) {
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
//1.找到待删除节点的之前的一个节点
Node prev = dummyHead;
for(int i=0; i");
curr = curr.next;
}
//循环写法二
// for(Node cur = dummyHead.next; curr!=null; curr=curr.next){
// res.append(curr + "->");
// curr = curr.next;
// }
res.append("NULL");
return res.toString();
}
public static void main(String[] args){
LinkdeList linkdedList = new LinkdeList<>();
linkdedList.addFist(000);
linkdedList.add(1,111);
linkdedList.add(2,222);
linkdedList.add(3,333);
linkdedList.add(4,444);
System.out.println(linkdedList);
linkdedList.remove(2);
// System.out.println(linkdedList);
// linkdedList.removeFirst();
// System.out.println(linkdedList);
// linkdedList.removeLast();
System.out.println(linkdedList);
}
}
(一)添加操作 add()
(二)删除操作 remove()
(三)修改操作 set(index, e) O(n)
(四)查找操作
以上所有内容都是通过"慕课网"听"liuyubobobo"的《玩转数据结构》课程后总结