今天和大家分享数据结构中链表的相关知识。在数据结构中有线性结构和非线性结构两种类别的区分,虽然链表属于线性结构是有序的列表,但是它在内存中却是不连续的。
关于数组和链表的优缺点。
数组静态分配内存,链表动态分配内存;数组在内存中连续,链表不连续;数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。
数组的优点
随机访问性强(通过下标进行快速定位)
查找速度快
数组的缺点
插入和删除效率低(插入和删除需要移动数据)
可能浪费内存(因为是连续的,所以每次申请数组之前必须规定数组的大小,如果大小不合理,则可能会浪费内存)
内存空间要求高,必须有足够的连续内存空间。
数组大小固定,不能动态拓展
链表的优点
插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素)
内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间)
大小没有固定,拓展很灵活。
链表的缺点
不能随机查找,必须从第一个开始遍历,查找效率低
链表分为单链表、双链表、循环链表。下面我就给大家一一介绍。
链表是以节点的方式来存储的,单链表每个节点包含data域(用来存放数据),next域(指向下一个节点),每个单链表都需要一个固定的头指针。在内存中不一定是连续存放的。
内存示意图如下所示。
逻辑结构示意图
小练习 实现单链表的增删改查
添加思路:单链表的添加比较简单我们只需要把添加的节点直接添加到链表的最后即可。直接将原来最后一个节点的next指向添加的节点。
首先我们先定义一个节点类
class CharacterNode{
public int id;
public String name;
public String nickname;
public CharacterNode next;
public CharacterNode(int id,String name,String nickname){
this.id = id;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "CharacterNode{" +
"id=" + id +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
然后创建一个用来管理节点的类,在这个类里写单链表的CRUD方法,然后我们通过遍历查看链表中的数据。遍历的时候首先要判断单链表是否为空。如果单链表中有数据才能输出。
public static void main(String[] args){
CharacterNode hero1 = new CharacterNode(1,"宋江","及时雨");
CharacterNode hero2 = new CharacterNode(2,"卢俊义","玉麒麟");
CharacterNode hero3 = new CharacterNode(3,"吴用","智多星");
CharacterNode hero4 = new CharacterNode(4,"林冲","豹子头");
/**
* 创建单链表
*/
SingleLinkedListManger sllm = new SingleLinkedListManger();
sllm.add(hero1);
sllm.add(hero1);
sllm.add(hero1);
sllm.add(hero1);
sllm.list();
}
class SingleLinkedListManger{
/**
* 先初始化一个头节点,头节点不要动,不存放具体数据
*/
private CharacterNode head = new CharacterNode(0,"","");
public CharacterNode getHead(){
return head;
}
/**
* 添加节点到单向链表
* 不考虑编号顺序时
* 1.找到当前链表的最后节点
* 2.将最后这个节点的next指向新的节点
*/
public void add(CharacterNode newNode){
/**
* 因为head节点不能动,因此我们需要一个辅助遍历temp
*/
CharacterNode temp = head;
//遍历链表,找到最后
while (true){
/**
* 当temp.next==null时表示找到链表最后
*/
if (temp.next==null){
break;
}
/**
* 如果没有找到最后,将temp后移
*/
temp = temp.next;
}
/**
* 当退出while循环时,temp就指向了链表的最后
* 将最后这个节点的next 指向新的节点
*/
temp.next = newNode;
}
/**
* 显示链表[遍历]
*/
public void list(){
/**
* 判断链表是否为空
*/
if (head.next == null){
System.out.println("链表为空");
return;
}
CharacterNode temp = head.next;
while (true){
if (temp == null){
break;
}
//输出节点的信息
System.out.println(temp);
//将temp后移
temp = temp.next;
}
}
}
按顺序插入,首先找到需要添加节点的位置,通过辅助变量来查找。然后,把需要把新节点的next指向原先节点的next,然后原先节点的next指向添加的节点。这样就可以实现按顺序插入。
newNode.next = temp.next
temp.next=newNode.next
public void addByOrder(CharacterNode newNode){
/**
* 因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
* 因为单链表,因为我们找的temp是位于 添加位置的前一个节点,否则插入不了
*/
CharacterNode temp = head;
/**
* flag标志添加的编号是否存在,默认为false
*/
boolean flag = false;
while (true){
/**
* 说明temp已经在链表的最后
*/
if (temp.next == null){
break;
}
/**
* 位置找到,就在temp的后面插入
*/
if (temp.next.id > newNode.id){
break;
/**
* 表示id已经存在
*/
}else if (temp.next.id == newNode.id){
flag = true;
break;
}
temp = temp.next;
}
if (flag){
System.out.println("准备插入的英雄的编号已经存在了,不能添加");
}else {
newNode.next =temp.next;
temp.next = newNode;
}
}
修改思路:根据id修改找到需要修改的节点,进行修改即可。
public void update(CharacterNode newNode){
if (head.next == null){
System.out.println("链表为空~");
return;
}
/**
* 根据no编号找到需要修改的节点
*/
CharacterNode temp = head.next;
boolean flag = false;
while (true){
if (temp == null){
break;
}
if (temp.id == newNode.id){
flag = true;
break;
}
temp = temp.next;
}
/**
* 根据flag判断是否找到要修改的节点
*/
if (flag){
temp.name = newNode.name;
temp.nickname = newNode.nickname;
}else {
System.out.printf("没有找到编号为%d的节点\n",newNode.id);
}
}
删除节点
思路:先找到需要删除这个节点的前一个节点temp.将temp的next指向要删除节点的next
temp.next = temp.next.next; 被删除的节点没有任何引用会被java的垃圾回收机制回收。
/**
* 删除节点
* @param no
*/
public void del(int id){
if (head.next == null){
System.out.println("链表为空~~");
return;
}
HeroNode temp = head;
boolean flag = false;
while (true){
if (temp.next == null){
break;
}
if (temp.next.id == id){
flag = true;
break;
}
temp = temp.next;
}
if (flag){
temp.next = temp.next.next;
}else {
System.out.printf("要删除的%d 节点不存在\n",id);
}
}