数据结构01- 单链表、双端链表、双向链表、无序链表、有序链表
0、节点
节点 是数据结构的基础,是数据结构的基础 构成复杂数据结构的基本组成单位。
public class Node {
public long data; public Node next; public Node(long value) {
this.data = value; } }
1.链表的定义
通常由一系列组成 每个节点包含任何实例数据(data fields)一两个用来指向 上一个 或 下一个 链接节点的位置(links”)。
以上是单链表的存储原理图,head作为第一个节点,它不存储任何数据,只是指向链表中存储数据的第一个节点,每个节点都有一个 next 引用,指向下一个节点,一个接一个地记录下来,直到最后一个节点 next 指向 null 。
2.链表的类型和特点
单链表的节点类
保留。链表类
(想想为什么?;
双端链表的节点类
保留。链表类
(想想为什么?;
双向链表的节点类
保留。链表类
;
如图所示:
最大的特点是 增加头部或尾部 新节点。
上面几种 链表都是 。
增加、减少或其他符合一定条件的规则,插入数据时,从头结点到每个节点,在符合规则的地方插入新节点。
3.普通链表、单链表(单向链表)
普通链表 ,也叫 单链表 或者 单向链表。
链表是一种数据存储结构。Java原生JDK链表也相应实现。 单向链表是链表之一,其主要原理是:
-
链表以节点为基础(Node)要保存的数据以节点对象的形式存储。
-
下一个节点被引用到单向链表中的每个节点(Node next),下一个节点可以通过上一个节点依次串联找到,所以如果你想遍历整个单向链表,你需要找到第一个节点。
-
**链表不同于数组,不一定是内存中的连续空间,**由于它是一个节点存储只需要持有下一个节点的地址,就可以利用内存中分散的空间进行保存。
-
很多人都知道链表比较相比List他添加和删除速度快,查询速度慢,主要是因为链表的修改只需要修改前持有的前节点和后节点的引用,而list删除或添加操作时,通常需要移动集合元素,因此需要很长时间。但是,事实上,如果不考虑随机插入,仍然需要根据实际情况进行判断,最后一直插入,list它非常快,因为不需要移动元素,但如果它是随机插入或插入在前面,链表有很大的优势,所以谁快谁慢或结合具体问题.
单链表 链表中最简单的结构。单链表的节点(Node)分为两部分,第一部分(data)保存或显示关于节点的信息,并将下一个节点的地址存储在另一部分。最后一个节点存储地址的部分指向空值。
- :单向链表 只有一个方向,一般需要从第一个节点开始每次访问下一个节点,直到需要的位置。
- :插入一个节点,对于单向链表,我们只提供插入链表头,只需将当前插入的节点设置为头节点,next指向原始节点。
- :删除节点,我们将节点的最后一个节点next指向节点的下一个节点。
查找图:
在表头添加节点:
删除节点:
基本实现
在JDK链表的集合容器已经包装在中间,我们可以直接使用,但手动实现一些基本功能仍然有助于更好地理解底层原理,加深对这种数据结构的理解。
3.0.
- 要实现链表,首先需要定义节点。根据以上分析,单向链表中的节点应持有下一个节点的参考值和要存储的数据值。为了更好地通用,这里的数据层应该包装Object但是为了方便这里没有包装。
/*** * 节点 */
class Node{
//************数据层************
//可封装为Object
public int no;
public String name ;
public String nick;
//******************************
//下一个节点的引用
public Node next;
public Node(int no , String name , String nick){
this.no = no ;
this.name = name ;
this.nick = nick ;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
", name='" + name + '\'' +
", nick='" + nick + '\'' +
'}';
}
}
首先我们需要声明一个头结点Head,这个head不存放具体的数据,只是记录第一个节点的地址。
public class LinkedList {
//声明一个头结点.这个节点不放具体的数据,只记录第一个节点的地址
//根据上面的Node的构造方法初始化,next指针先不赋值.
private Node head = new Node(0 , "" , "");
}
- 思路分析
- 首先我们需要声明一个第三变量temp来作为指针,开始时默认指向头结点,添加新节点时循环遍历链表,每循环一次,temp就后移一位,找到链表的最后一个节点,找到最后一个节点之后只需将原来的最后一个节点的next指向新加节点即可,判断是否是最后一个节点的条件应该是temp的next值为null,这样新加的节点就变成了最后一个节点,这里需要保证新加节点的next值为null.
public class LinkedList {
//声明一个头结点.这个节点不放具体的数据,只记录第一个节点的地址
private Node head = new Node(0 , "" , "");
/** * 添加方法,这里的添加需要保证插入的Node是新的next是null,如果next的值不为空之前使用过指向另一个 * Node地址的话会出现添加一个却把之前链表都连起来的情况,如果恰巧形成了闭环,首尾相连会出现死循环的情况 * @param newNode * @return */
public boolean add(Node newNode){
//声明一个第三变量用作指针,开始默认指向头结点
Node temp = head;
//循环遍历链表,将新节点加入到next为null的节点后,就是直接加入到最后一个节点的后面
while(true){
//如果当前节点的下一个节点是null ,说明该节点是最后一个节点
if(temp.next == null){
//直接将新节点添加到最后一个节点之后,next指向新节点
temp.next = newNode;
return true;
}
//将指针后移一位,指针指向下一个节点
temp = temp.next ;
}
}
}
- 这个只需要判断头结点的next是否为null即可
/** * 判断链表是否为空的方法 * @return */
public boolean isEmpty(){
//如果头结点的下一个是空,就说明链表是空的
return head.next == null ;
}
- 思路
- 首先要根据条件找到要修该的节点,这里用no值查询作为条件
- 仍然是声明第三变量temp作为指针,循环链表,如果no值与链表中的相同就说明找到了要修改的节点.
- 在这里修改有两种思路,第一种是temp指向当前节点,修改当前节点的数据,第二种是temp指向当前节点的上一个节点,也就是说temp.next才是要修改的节点,那么可以将temp.next指向新的节点,新的节点next指向temp.next.next,即原来要修改节点的next,就达到了替换的作用,这里采用第二种方式
public boolean update(Node newNode) {
//首先需要找到要修改的节点
Node temp = head;
//如果为空直接返回
if(isEmpty()){
System.out.println("链表为空");
return false;
}
//遍历链表
/* 如果当前节点的下一个节点的no值和要修改的节点的no值相同,说明当前节点的下一个节点就是要修改的 节点,此时的temp指针指向的是要修改节点的前一个节点,只需要将前一个节点的next指向新的节点,然后 将新的节点的next指向原来要修改的节点的next,就是将要修改的节点的next值赋给新的节点 */
while(temp.next != null){
//如果找到了相同的no值说明找到了要修改的节点
if(temp.next.no == newNode.no){
/* 首先将要修改节点的next值赋给新节点 , 此时temp.next指向的是要修改的节点,所以要修改节点的 next的值是temp.next.next */
newNode.next = temp.next.next;
//然后将当前节点的next指向新的节点
temp.next = newNode;
return true;
}
//指针后移
temp = temp.next;
}
System.out.println("没有找到该节点");
return false;
}
- 思路
- 首先需要明确,单向链表节点的删除是必须靠上一个节点来完成,因为,单向链表是单向的,只能找当前的后面的节点,不能找到前面的节点,而链表的删除则是需要将当前要删除的节点的上一个节点的next的值,指向要删除节点的下一个,由于无法通过当前节点找到上一个节点,所以这里的指针应该指向要删除节点的上一个节点,使用temp.next = temp.next.next的方式.
/** * 删除方法,根据no值来删除 * 该方法与之前修改方法类似,只需将要删除节点的的上一个next直接指向要删除节点的next * @return */
public boolean del(int no){
//第三变量默认指向头结点
Node temp = head;
if(isEmpty()){
System.out.println("链表为空");
return false;
}
while(temp.next != null){
//如果找到了要删除的节点
if(temp.next.no == no){
//就将当前节点的next指向要删除的的next
//注意此时的temp指向的是要删除节点的前一个
temp.next = temp.next.next;
return true ;
}
//指针后移
temp = temp.next;
}
System.out.println("没找到要删除的节点");
return false;
}
- 思路
- 循环,利用temp指针,每循环一次temp后移一位,当temp.next为null说明链表结束.
/** * 遍历方法 */
public void showList(){
Node temp = head;
if (isEmpty()){
System.out.println("链表为空");
}
while(temp.next != null){
System.out.println(temp.next);
temp = temp.next;
}
}
- 思路
- 定义一个计数器,每循环一次,计数器++,仍然是当temp.next为空说明循环结束
/** * 统计头结点的个数,不包含头结点 */
public int size(){
int size = 0 ;
Node temp = head.next;
while(temp != null){
size++;
temp = temp.next;
}
return size;
}
综合代码
public class LinkedList {
//声明一个头结点.这个节点不放具体的数据,只记录第一个节点的地址
private Node head = new Node(0 , "" , "");
/** * 添加方法,这里的添加需要保证插入的Node是新的next是null,如果next的值不为空之前使用过指向另一个 * Node地址的话会出现添加一个却把之前链表都连起来的情况,如果恰巧形成了闭环,首尾相连会出现死循环的情况 * @param newNode * @return */
public boolean add(Node newNode){
//声明一个第三变量用作指针,开始默认指向头结点
Node temp = head;
//循环遍历链表,将新节点加入到next为null的节点后,就是直接加入到最后一个节点的后面
while(true){
//如果当前节点的下一个节点是null ,说明该节点是最后一个节点
if(temp.next == null){
//直接将新节点添加到最后一个节点之后,next指向新节点
temp.next = newNode;
return true;
}
//将指针后移一位,指针指向下一个节点
temp = temp.next ;
}
}
/** * 判断链表是否为空的方法 * @return */
public boolean isEmpty(){
//如果头结点的下一个是空,就说明链表是空的
return head.next == null ;
}
/** * 修改链表中数据的方法 * 需要提供一个新的节点,根据新节点的id值找到原来的节点,将原数据进行修改,id值不变 * @return */
public boolean update(Node newNode) {
//首先需要找到要修改的节点
Node temp = head;
//如果为空直接返回
if(isEmpty()){
System.out.println("链表为空");
return false;
}
//遍历链表
/* 如果当前节点的下一个节点的no值和要修改的节点的no值相同,说明当前节点的下一个节点就是要修改的 节点,此时的temp指针指向的是要修改节点的前一个节点,只需要将前一个节点的next指向新的节点,然后 将新的节点的next指向原来要修改的节点的next,就是将要修改的节点的next值赋给新的节点 */
while(temp.next != null){
//如果找到了相同的no值说明找到了要修改的节点
if(temp.next.no == newNode.no){
/* 首先将要修改节点的next值赋给新节点 , 此时temp.next指向的是要修改的节点,所以要修改节点的 next的值是temp.next.next */
newNode.next = temp.next.next;
//然后将当前节点的next指向新的节点
temp.next = newNode;
return true;
}
//指针后移
temp = temp.next;
}
System.out.println("没有找到该节点");
return false;
}
/** * 遍历方法 */
public void showList(){
Node temp = head;
if (isEmpty()){
System.out.println("链表为空");
}
while(temp.next != null){
System.out.println(temp.next);
temp = temp.next;
}
}
/** * 删除方法,根据no值来删除 * 该方法与之前修改方法类似,只需将要删除节点的的上一个next直接指向要删除节点的next * @return */
public boolean del(int no){
//第三变量默认指向头结点
Node temp = head;
if(isEmpty()){
System.out.println("链表为空");
return false;
}
while(temp.next != null){
//如果找到了要删除的节点
if(temp.next.no == no){
//就将当前节点的next指向要删除的的next
//注意此时的temp指向的是要删除节点的前一个
temp.next = temp.next.next;
return true ;
}
//指针后移
temp = temp.next;
}
System.out.println("没找到要删除的节点");
return false;
}
/** * 统计头结点的个数,不包含头结点 */
public int size(){
int size = 0 ;
Node temp = head.next;
while(temp != null){
size++;
temp = temp.next;
}
return size;
}
}
/*** * 节点 */
class Node{
//************数据层************
public int no;
public String name ;
public String nick;
//******************************
public Node next;
public Node(int no , String name , String nick){
this.no = no ;
this.name = name ;
this.nick = nick ;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
", name='" + name + '\'' +
", nick='" + nick + '\'' +
'}';
}
}
测试自定义单向链表
public class Test {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
Node n1 = new Node(1 , "宋江" , "及时雨");
Node n2 = new Node(2 , "卢俊义" , "玉麒麟");
Node n3 = new Node(3 , "吴用" , "智多星");
Node n4 = new Node(4 , "李逵" , "黑旋风");
Node n5 = new Node(5 , "林冲" , "豹子头");
linkedList.addByOrder(n1);
linkedList.addByOrder(n3);
linkedList.addByOrder(n2);
linkedList.addByOrder(n5);
linkedList.addByOrder(n4);
System.out.println("修改之后~~~~~~~");
Node n6 = new Node(5 , "冲冲" , "豹子头~~~");
linkedList.update(n6);
linkedList.showList();
System.out.println("移除之后~~~~~~~");
linkedList.del(5);
linkedList.del(1);
linkedList.del(2);
linkedList.del(3);
linkedList.del(4);
linkedList.showList();
//之前的节点不能使用,需要新建节点,保证next值为null
Node n11 = new Node(1 , "宋江" , "及时雨");
Node n7 = new Node(2 , "卢俊义" , "玉麒麟");
Node n8 = new Node(3 , "吴用" , "智多星");
Node n9 = new Node(4 , "李逵" , "黑旋风");
Node n10 = new Node(5 , "林冲" , "豹子头");
System.out.println("添加之后~~~~~~~");
linkedList.add(n11);
linkedList.add(n7);
linkedList.add(n8);
linkedList.add(n9);
linkedList.add(n10);
linkedList.showList();
运行结果正确,可以实现基本功能
4、双端链表(单向引用)
双端链表的节点类保留下一节点的引用。(单向引用)
保留对头节点、尾节点的引用,可以从尾节点插入,但只能从头节点删除,只能从一个方向遍历,相当于单向链表多了一个对尾节点的引用。
双端链表的含有对 第一个节点 和 最后一个节点的引用 。
|
双端链表的操作 | 读取(引用) | 插入 | 删除 |
---|---|---|---|
头部 | √ | √ | √ |
尾部 | √ | √ | x |
只能从一个方向遍历,相当于 单向链表 多了一个对尾节点的引用。
package node2; public class Node { public long data; public Node next; public Node(long data) { this.data = data; } // 显示方法 public void display() { System.out.print(data + " "); } } package node2; /** * 双端链表 */ public class