数据结构(2)-链表
填补那些模棱两可的后知后觉
什么是链表
链表是一种用于存储数据集合的数据结构,他是最简单的动态数据结构。上一篇我们虽显然实现了一个简单的动态数组。单这仅仅是面向使用者而言,其实数组的底层还是维护的是一个静态的数组,我们只是简单的通过拷贝的当时实现容量的增减,但是!!!链表 则是真正意义上的动态数据结构。
链表的优点
- 真正的动态数据结构,不需要处理固定容量的问题
- 能够在常数时间内扩展容量
链表的缺点
- 随机访问效率较低
- 链表中需要维护一份指针引用,相对浪费内存
链表与数组的简单对比
- 数组最用用于索引有语意的情况,其最大特点就是支持快速的查询和修改
- 链表不适用于索引有语意的情况,其最大的特点则是动态,能快读插入和删除数据
实现一个自己的链表类
同样,我们和上一篇数组的介绍中,我们来完成一个自已的链表,支持最简单的增删该查功能。
public class LinkedList<E> {
private class Node{
private E e;
private 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 LinkedList(){
dummyHead = new Node();
size = 0;
}
//获取链表中的元素个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return size == 0;
}
/**
* 在指定索引处添加元素
* @param index
* @param e
*/
public void add(int index,E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
//利用虚拟头节点,将添加索引处的前方元素全部前移一位
Node prve = dummyHead;
for (int i = 0; i < index; i++) {
//将原元素的指针指向原来的位置
prve = prve.next;
}
prve.next = new Node(e,prve.next);
size++;
}
/**
* 新增头节点
*/
public void addFirst(E e){
// Node node = new Node(e);
// node.next = dummyHead;
// dummyHead = node;
dummyHead = new Node(e,dummyHead);
}
/**
* 在结尾处增加元素
* @param e
*/
public void addLast(E e){
this.add(size,e);
}
public E get(int index){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node cur = dummyHead.next;
for (int i = 0; i < size; i++) {
cur = cur.next;
}
return cur.e;
}
//获取链表的第一个元素
public E getFirst(){
//可以直接返回虚拟头节点的指向的下一个元素
return dummyHead.next.e;
}
//获取链表的最后一个元素
public E getLast(){
return this.get(size -1);
}
//修改链表中指定位置的元素
public void set(int index ,E e){
//思路与get方法类似,找到改元素,直接替换即可
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node cur = dummyHead.next;
for (int i = 0; i < size; i++) {
cur = cur.next;
}
cur.e = e;
}
//查找链表中是否包含某个元素e
public boolean contains(E e){
//从第一个元素开始遍历,然后进行匹配
Node cur = dummyHead.next;
while (cur != null){
if(cur.e.equals(e)){
return true;
}
cur = cur.next;
}
return false;
}
//删除指定位置的元素
public void remove(int index){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
//首先定位此元素,与修改或查询一致,将改元素置空,然后将改元素原来的前后元素进行指向
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next ;
}
//查询到了元素
Node retNode = prev.next;
//将前后元素进行关联
prev.next = retNode.next;
//直接指向空
retNode.next = null;
size --;
}
//删除链表中的第一个元素
public void removeFirst(){
//将虚拟头节点指向第二个元素即可
dummyHead.next = dummyHead.next.next;
size--;
}
// 从链表中删除最后一个元素, 返回删除的元素
public void removeLast() {
remove(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while (cur != null) {
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL");
return res.toString();
}
}
链表中虚拟头节点的作用
因为第一个元素(真头节点)是没有前序节点的。当我们在做操作时都需要对头节点进行单独的判断,这样一来的话,真头节点的逻辑就有空。所以为了方便,我们设置一个虚拟的头节点,来屏蔽真头节点的特殊性。
简单复杂度分析
我们从链表的操作中可以很容易的看出,对于增删改查这几个操作的复杂度都是O(n)的,但是如果我们只是对链表头进行增/删/查的操作的话,那么它的复杂度就是O(1)的,这里也可以看出来我们的链表适合干的事情了..
作者:黑米面包派
同步更新: https://www.wujiwen.cn
欢迎一起交流进步