最近学习了集合的实现,相比于数组,Java的集合的概念我就不去详细说了,我就简单说一下我对集合的理解
1.集合是一个可以动态存放数据,不像数组一样,一旦确定了长度就不可变了
2.数组需要存放相同数据结构的数据,集合存放的都是Object类型的,也就是引用数据结构
3.无法直接获取数组实际存储的元素个数,length用来获取数组的长度,但集合可以通过size()直接获取集合实际存储的元素个数。
差不多就先概况一下这些基础
在Java中集合的实现是基于Collection这个超级接口来实现的,这里我就不详细讲述Collection了,大家可以去看看大佬们对它的详解
在Collection集合中主要分为有序集合和无序集合,有序集合存放的数据是有下标索引且可以重复的,无序集合存放的数据是没有下标索引且不可重复
而我今天是针对有序集合中的链表数据结构进行一下我的讲解
ps:在集合中是基于双向链表实现的
好了现在让我先来讲一下什么是单向链表,后面我再写一下双向链表
单向链表与数组不同,数组存放数据的内存地址是连续的,而链表存放数据的内存地址不是连续的
所以我们可以知道,数组擅长查询,因为数组的查询效率高,无论有多少数据,它查询的时间复杂度都是一样的,因为数组是获取了数组头的内存地址,通过计算来知道数组其他数据的内存地址的但是对于增删来说,数组的效率很低,因为它需要保持数据内存地址的连贯性,会进行数据的移动
而对于链表来说是和数组相反的,它的查询效率低,增删效率高,我来画一幅图简单说一下
(如果想更清楚的了解数据结构,大家搜搜大佬们的讲解,我这里就是为了实现简单说一下)
链表是通过节点与节点的连接组成的,节点又分为两个部分
一个是当前节点存放的数据
一个是连接的下一个节点的内存地址
在这个链表中,张三是头部节点,赵六是尾部节点
所以我们可以看见
张三(0x10) 指向 李四(0x11)
李四(0x11) 指向 王五(0x12)
王五(0x12) 指向 赵六(0x13)
而赵六作为尾部节点,所以它的下一个节点为null,是空的
为什么说链表的效率高,这里举例,我们要删除王五这个节点,再画一个图:
我们删除一个节点,只需要将这个节点与它前后节点的连接链条断掉就可以了,所以我们可以直接将李四指向赵六。这个过程就是,
李四连接王五,所以李四可以获取到王五的下一个节点是赵六,也就是说,
李四获取到了王五的下一个节点的内存地址(赵六0x13)
将李四存放的指向下一个节点的内存地址修改为赵六(0x13)
将赵五置空
我们可以看到,没有任何一个节点发生了位置的改变
下面我们如果再把王五增加过来呢?
原理其实也是一样的,断开李四和赵六的连接
王五先获取李四存放的指向下一个节点的内存地址(赵六0x13)
然后将李四的指向下一个节点的内存地址修改为王五(0x12)
再将王五的指向下一个节点的内存地址修改为赵六(0x13)
在此之前我先简单说一下泛型
private static class Node1{
String data;
Node next;
}
private static class Node<E>{
E data;
Node<E> next;
Node1和Node的区别就是规定了Node这个对象的数据类型
我们先来看Node1,Node1中的data是String,字符串类型,它还可以是别的引用类型对吗
来看Node中的data是E,所以泛型就是给了它们一个概况,不提前规定类的数据类型,在创建的时候规定
Node<String> mynode = new Node<>();
这时E就为String也就是字符串类型,所以data也是字符串类型
我这里就简单说一下,大家不懂去看看泛型的使用。
我们通过这个小例子应该能理清链表增删的实现了,下面我开始用Java写一个链表出来,代码如下
public class Mylink<E> {
private int size;
private Node<E> first;
public Mylink() {
}
public int size() {
return size;
}
//向单向链表的末尾添加一个元素
public void add(E data){
//如果first是空,证明这是一个空列表
Node<E> newNode = new Node<>(data,null);
if (first == null){
first = newNode;
size++;
return;
}
//找到末尾节点
Node<E> last = findLast();
//新建节点,并让末尾节点指向新节点
last.next = newNode;
size++;
}
//找到链表的末尾节点
private Node<E> findLast() {
//遍历链表,从头节点开始判断next是否为空,不为空则继续遍历寻找
Node<E> last = first;
//因为最后一个节点的next一定是null
while (last.next != null){
last = last.next;
}
return last;
}
public void addFirst(Node<E> newNode){
newNode.next = first;
size++;
}
//向单向链表的指定位置添加一个元素
public void add(int index,E data){
//新建节点对象
Node<E> newNode = new Node<>(data,null);
//根据下标找到对应的节点对象,上一个对象
if (index < size()){
if (index != 0){
Node<E> prev = node(index -1);
newNode.next = prev.next;
prev.next = newNode;
size++;
}else {
addFirst(newNode);
}
}else {
System.out.println("下标越界,无信息");
}
}
//1,2,3,4,5,6
//0,1,2,3,4,5
//返回索引处的节点对象
private Node<E> node(int index) {
Node<E> next = first;
for (int i = 0; i < index; i++) {
next = next.next;
}
return next;
}
//删除指定位置的元素
public void remove(int index){
//假如删除的是头节点
if (index == 0){
Node<E> oldFirst = first;
first = first.next;
oldFirst.next = null;
oldFirst.data = null;
size--;
return;
}
//删除的节点
Node<E> removeNode = node(index);
//删除的上一个节点
Node<E> prevNode = node(index-1);
prevNode.next = removeNode.next;
removeNode.next = null;
removeNode.data = null;
size--;
}
//修改指定位置的元素
public E set(int index,E data) {
//将修改的节点赋值到setNone
Node<E> setNone = node(index);
//保存修改的数据
E oldData = setNone.data;
//修改数据
setNone.data = data;
return oldData;
}
//根据下标获取数据
public E get(int index){
return node(index).data;
}
public void setSize(int size) {
this.size = size;
}
public Node<E> getFirst() {
return first;
}
public void setFirst(Node<E> first) {
this.first = first;
}
private static class Node<E>{
//节点内的数据
E data;
//节点指向的下一个对象
Node<E> next;
public Node(E data, Node<E> next) {
this.data = data;
this.next = next;
}
}
}
让我来解释一下这段代码:
首先我们创建一个链表类
public class Mylink<E> {
private int size;
private Node<E> first;
并定义了两个属性,一个是size:用来记录链表的节点个数
一个是first:用来标记链表的头节点(因为不知道头节点,是没有办法确定从哪里开始,也就是遍历查找)
这里的Node我设置为内部类了,大家也可以再自定义一个类哈
//单向链表的节点
private static class Node<E>{
//节点内的数据
E data;
//节点指向的下一个对象
Node<E> next;
public Node(E data, Node<E> next) {
this.data = data;
this.next = next;
}
}
定义一个静态内部类,也就是链表的节点,节点有两个属性,大家看到这里应该也知道是什么了,一个是节点保存的数据,一个是节点指向的下一个对象。
ps:我这里再多说一嘴,可能大部分看我的都是刚开始学习Java的,我们要知道,链表是一个对象,所以节点同样也是对象,是链表内的对象,每个节点都是独立的,他们是依赖关系。
public Mylink() {
}
public int size() {
return size;
}
这是链表的无参构造函数会默认给链表的属性赋默认值,所以就是初始化一个空的链表
size():就是返回当前链表的节点个数
添加方法:(默认向末尾添加)
//向单向链表的末尾添加一个元素
public void add(E data){
//如果first是空,证明这是一个空列表
Node<E> newNode = new Node<>(data,null);
if (first == null){
first = newNode;
size++;
return;
}
//找到末尾节点
Node<E> last = findLast();
//新建节点,并让末尾节点指向新节点
last.next = newNode;
size++;
}
添加一个节点,首先要构建出一个节点对象,data为添加的数据,null为节点指向下个节点的地址,默认为空。
在添加的时候要先判断这是不是一个空链表,而判断空链表只需要判断有没有头节点,如果头节点为null证明这是一个空链表
如果是空链表,那这次添加的节点是不是就变成了链表的头节点,那我们让first,也就是头节点变成newNode,因为我们添加了一个节点,所以链表的节点数量要增加,所以size++
如果不是空列表,我们需要知道末尾节点这个对象的地址,然后更改它的next,也就是指向本次添加的节点,同样的,size++
下面我们讲解一下findLast这个方法:
//找到链表的末尾节点
private Node<E> findLast() {
Node<E> last = first;
//因为最后一个节点的next一定是null
while (last.next != null){
last = last.next;
}
return last;
}
之后的讲解,next就是指向下一个节点的内存地址,data就是该节点的存储内容
我们要先知道,怎么样算是找到了末尾节点,末尾节点的next属性一定是null
所以我们要从头开始寻找,可以将头节点看作末尾节点,还是用刚刚的例子吧:
我们将头节点张三,看作末尾节点,判断它的next是不是空,它不是,所以这时last变量变成了李四
我们再将李四看作末尾节点,判断它的next是不是空,它不是,所以这时last变量变成了王五
以此类推,这时找到了赵六,last = 赵六节点,赵六的next为null,所以它是尾部节点,将赵六返回。
添加方法:(头部添加)
public void addFirst(Node<E> newNode){
newNode.next = first;
size++;
}
头部添加就比较简单了,只需要将新创建的节点的next改成first不就好了,同样的size++
添加方法:(按照索引添加)
private Node<E> node(int index) {
Node<E> next = first;
for (int i = 0; i < index; i++) {
next = next.next;
}
return next;
}
首先我们来看如何找到指定索引下的节点对象
还是一样的,要从头开始,将头节点赋值next,假如我们指定的索引为2,获取王五节点对象
我们是不是只需要获取到它的上一个节点
因为上一个节点的next,不就是我们需要的节点对象吗
我们指定的索引是2,而恰好只需要循环2次,就可以找到目标节点的,上一个节点对象
所以再去看代码就可以看懂了
下面是实现:
//向单向链表的指定位置添加一个元素
public void add(int index,E data){
//新建节点对象
Node<E> newNode = new Node<>(data,null);
//根据下标找到对应的节点对象,上一个对象
if (index < size()){
if (index != 0){
Node<E> prev = node(index -1);
newNode.next = prev.next;
prev.next = newNode;
size++;
}else {
addFirst(newNode);
}
}else {
System.out.println("下标越界,无信息");
}
}
一样的,添加就要新建一个节点对象。我这里严谨了一下添加了判断,防止下标越界,因为假如一共5个数据,而你指定的索引为5,是没有这个数据的。
然后在里面再加一个判断,如果传入的是0,就直接调用头部添加
如果是非0,我们需要获取到上一个节点对象,所以要将索引的值-1,这时再将添加的节点对象的next修改成上一个节点对象的next,参考我上面的举例,然后再让上一个节点对象,指向新节点,最后个数加1
删除节点
//删除指定位置的元素
public void remove(int index){
//假如删除的是头节点
if (index == 0){
Node<E> oldFirst = first;
first = first.next;
oldFirst.next = null;
oldFirst.data = null;
size--;
return;
}
//删除的节点
Node<E> removeNode = node(index);
//删除的上一个节点
Node<E> prevNode = node(index-1);
prevNode.next = removeNode.next;
removeNode.next = null;
removeNode.data = null;
size--;
}
先进行一下判断,是不是删除的为头节点,因为头节点是没有节点指向它的,也就是说它没有上一个节点,首先将头节点对象保存到oldfirst中,然后更改头节点对象,变成头节点的next,然后将oldfirst的数据和next置空
如果不是头节点,我们就要先获取该节点的上一个节点,然后将上一个节点的next修改为删除节点的next,不懂可以翻上去看一下举例。然后将数据和next置空,size--
修改与查找
//修改指定位置的元素
public E set(int index,E data) {
//将修改的节点赋值到setNone
Node<E> setNone = node(index);
//保存修改的数据
E oldData = setNone.data;
//修改数据
setNone.data = data;
return oldData;
}
//根据下标获取数据
public E get(int index){
return node(index).data;
}
这里我就将修改和查找一起说了,上面理解了这块就很简单了,修改中,调用node方法获取目标节点对象,保存下来,然后修改节点的data值,返回修改之前的值,方便查看。
查询就是直接调用node方法查询目标节点对象的data值
下面是一段测试程序:
public class Text {
public static void main(String[] args) {
//创建一个单向链表
Mylink<String> mylink = new Mylink<>();
//添加元素
mylink.add("zhangsan");
mylink.add("lisi");
mylink.add("wangwu");
mylink.add("zhaoliu");
//在指定位置增加元素
mylink.add(1,"李四");
for (int i = 0; i < mylink.size(); i++) {
System.out.println(mylink.get(i));
}
System.out.println("==============");
//删除下标的元素
mylink.remove(1);
for (int i = 0; i < mylink.size(); i++) {
System.out.println(mylink.get(i));
}
System.out.println("==============");
//修改
System.out.println("修改的内容为:" + mylink.set(1,"李四"));
System.out.println("==============");
for (int i = 0; i < mylink.size(); i++) {
System.out.println(mylink.get(i));
}
}
}
这样一个单向列表就完成了
如果我有不对的地方感谢大家的指出,谢谢大家的观看!