数据结构-链表

820 阅读6分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

链表

物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现
每个元素至少包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域
链表的主要优势有两点:一是插入及删除操作的时间复杂度为O(1);二是可以动态改变大小

优点

链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;

缺点

因为含有大量的指针域,占用空间较大; 查找元素需要遍历链表来查找,非常耗时

结构

单向链表

单向链表(单链表)是链表的一种,它由节点组成,每个节点都包含下一个节点的指针
/**
* 节点信息
*/
public class OneWayNode<T> {
    private T  data; // 节点数据
    private OneWayNode<T> next; // 下一个节点地址
    public OneWayNode(T data) {
        this.data = data;
    }
    public OneWayNode<T> getNext() {
         return next;
    }
    public void setNext(OneWayNode<T> next) {
        this.next = next;
    }
    public T getData() {
        return this.data;
    }
    public void setData(T data) {
        this.data = data;
    }
}


public class OneWayLinkeList<T> {
    private static OneWayNode headNode = null;	// 头结点
    private static OneWayNode endNode = null;	// 尾结点
    private static int size = 0;		// 链表长度
    public void show() { // 遍历
        OneWayNode item = this.headNode;
        while(item!=null) {
            System.out.print(item.getData() + " ");
            item = item.getNext();
        }
    }
}

手撸代码-添加

添加操作有三种情况

  • 头部添加:先将头结点设置为新节点的下个节点,再将新节点设置为头结点
  • 尾部添加:直接将尾部结点的下个节点设置为新节点
  • 中间添加:先找到要添加的位置,然后将当前位置上个节点的下一个节点设置为新节点,再新节点的下一个节点设置为当前位置的节点
public static void main(String[] args) {
    OneWayLinkeList linkeList =	new OneWayLinkeList<String>();
    linkeList.add("hello");
    linkeList.add("hello2");
    linkeList.add("hello3");
    linkeList.show(); // hello1 hello2 hello3 
}
public void add(T data, int index) {
    if (index > this.size) {
        System.out.println("下标越界!");
    }
    this.size ++;
    OneWayNode node = new OneWayNode<T>(data);
    OneWayNode item = null;
    if (index == 0) {	// 头部添加
        if (this.headNode == null) {
            this.headNode = node;
            this.endNode = node;
        } else {
            item = this.headNode;
            this.headNode = node;
            this.headNode.setNext(item);
        }
        return;
    }
    if (index == this.size-1) { // 尾部添加
        item = this.endNode;
        item.setNext(node);
        this.endNode = node;
        return;
    }
    int x = 0;
    item = this.headNode;
    OneWayNode lastNode = null;
    while(item!=null) {	// 中间添加
        if (x==index) {
            lastNode.setNext(node);
            node.setNext(item);
            break;
        }
        lastNode = item;
        item = item.getNext();
        x++;
    }
}

手撸代码-修改

修改:先找到需要修改节点的位置,再将当前位置的上个节点的下个节点设置为新的节点,当前节点的下个节点设置为新节点的下个节点

最后还需要判断是否头部还是尾部

public static void main(String[] args) {
    OneWayLinkeList linkeList =	new OneWayLinkeList<String>();
    linkeList.add("hello1");
    linkeList.add("hello2");
    linkeList.add("hello3");
    linkeList.update("hello4", 2);
    linkeList.show(); // hello1 hello2 hello4 
}
public void update(T data, int index) {
    if (index > this.size) {
        System.out.println("下标越界!");
    }
    OneWayNode node = new OneWayNode<T>(data);
    int x = 0;
    OneWayNode item = this.headNode;
    OneWayNode lastNode = null;
    while(item!=null) {	// 修改
        if (x==index) {
            lastNode.setNext(node);
            node.setNext(item.getNext());
            if (index == 0) {
                this.headNode = node;
            }
            if (index == this.size - 1) {
                this.endNode = node;
            }
            break;
        }
        lastNode = item;
        item = item.getNext();
        x++;
    }
}

手撸代码-删除

删除也存在三种情况

  • 头部删除:直接将头部节点的下一个节点设置为头节点
  • 尾部删除:需要从头部节点开始遍历整个链表,遍历到最后节点时,将最后节点的上一个节点的下一个节点数据设置为null;
  • 中间删除:需要从头结点开始遍历链表,找到需要删除的节点时,将上一个节点的下个节点数据设置为当前删除节点的下一个节点数据
  • 三种删除可以共用一个方法,头部删除第一个节点就可以拿到,中间删除和尾部删除都需要遍历链表,尾部需要遍历整个链表O(∩_∩)O哈哈~
public static void main(String[] args) {
    OneWayLinkeList linkeList =	new OneWayLinkeList<String>();
    linkeList.add("hello1");
    linkeList.add("hello2");
    linkeList.add("hello3");
    linkeList.delete("hello2");
    linkeList.show(); // hello1 hello3
}
public void delete(T data) {
    OneWayNode item = this.headNode;
    OneWayNode lastNode = null;
    while(item!=null) {
        if (item.getData().equals(data)) {
            if (lastNode==null) {
                 this.headNode = item.getNext();
                 break;
            }
            lastNode.setNext(item.getNext());
            if (item.getNext() == null) {
                this.endNode = lastNode;
            }
            break;
        }
        lastNode = item;
        item = item.getNext();
    }
}

手撸代码-查询

根据索引查询需要遍历链表,根据内存查询一样需要遍历链表(^▽^)

public static void main(String[] args) {
    OneWayLinkeList linkeList =	new OneWayLinkeList<String>();
    linkeList.add("hello1");
    linkeList.add("hello2");
    linkeList.add("hello3");
    linkeList.get(1); // hello2
}
public void get(int index) {
    int x = 0;
    OneWayNode item = this.headNode;
    while (item != null) {
        if (x==index) {
            System.out.println(item.getData());
            break;
        }
        item = item.getNext();
        x++;
    }
}

双向链表

双链表也是由节点组成,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱;
两个指针比较浪费空间,但是可以双向遍历,提高了链表灵活性
public class TwoWayNode<T> {
    private T  data;	// 节点信息
    private TwoWayNode<T> next; // 下个节点
    private TwoWayNode<T> last; // 上个节点
    public TwoWayNode(T data) {
        this.data = data;
    }
    public TwoWayNode<T> getNext() {
         return next;
    }
    public void setNext(TwoWayNode<T> next) {
        this.next = next;
    }
    public TwoWayNode<T> getLast() {
         return last;
    }
    public void setLast(TwoWayNode<T> last) {
        this.last = last;
    }
    public T getData() {
        return this.data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

public class TwoWayLinkeList<T> {
	private static TwoWayNode headNode = null;	// 头结点
	private static TwoWayNode endNode = null;	// 尾结点
	private static int size = 0;				// 链表长度
}

手撸代码-添加

添加操作也有三种情况

  • 头部添加:跟单向链表一样
  • 尾部添加:跟单向链表有些不同,先直接将尾部结点的下个节点设置为新节点,然后新节点设置上一个节点为之前的尾节点
  • 中间添加:先找到要添加的位置
    1、然后将当前位置上个节点last节点的下一个节点设置为新节点,
    2、新节点的上个节点设置为last节点,
    3、再新节点的下一个节点设置为当前位置的节点next节点,
    4、最后将next节点的上个节点设置为新节点
public void add(T data, int index) {
    if (index > this.size) {
        System.out.println("下标越界!");
    }
    this.size ++;
    TwoWayNode node = new TwoWayNode<T>(data);
    TwoWayNode item = null;
    if (index == 0) {	// 头部添加
        if (this.headNode == null) {
            this.headNode = node;
            this.endNode = node;
        } else {
            item = this.headNode;
            item.setLast(node);
            node.setNext(item);
            this.headNode = node;
        }
        return;
    }
    if (index == this.size-1) { // 尾部添加
        item = this.endNode;
        item.setNext(node);
        node.setLast(item);
        this.endNode = node;
        return;
    }
    int x = 0;
    item = this.headNode;
    while(item!=null) {	// 中间添加
        if (x==index) {
            item.getLast().setNext(node);
            node.setLast(item.getLast());
            node.setNext(item);
            item.setLast(node);
            break;
        }
        item = item.getNext();
        x++;
    }
}

手撸代码-修改

修改逻辑跟单向链表一样,最后再加个步骤:将当前修改节点的上个节点指向上个位置的节点

最后还需要判断是否头部还是尾部

public void update(T data, int index) {
    if (index > this.size) {
        System.out.println("下标越界!");
    }
    TwoWayNode node = new TwoWayNode<T>(data);
    int x = 0;
    TwoWayNode item = this.headNode;
    TwoWayNode lastNode = null;
    while(item!=null) {	// 修改
        if (x==index) {
            item.getLast().setNext(node);
            node.setLast(item.getLast());
            node.setNext(item.getNext());
            item.getNext().setLast(node);
            if (index == 0) {
                this.headNode = node;
            }
            if (index == this.size - 1) {
                this.endNode = node;
            }
            break;
        }
        lastNode = item;
        item = item.getNext();
        x++;
    }
}

手撸代码-删除

public void delete(T data) {
    TwoWayNode item = this.headNode;
    TwoWayNode lastNode = null;
    while(item!=null) {
        lastNode = item.getLast();
        if (item.getData().equals(data)) {
            if (lastNode==null) {
                 this.headNode = item.getNext();
                 break;
            }
            if (item.getNext() == null) {
                lastNode.setNext(null);
                this.endNode = lastNode;
                break;
            }
            lastNode.setNext(item.getNext());
            item.setLast(lastNode);
            break;
        }
        item = item.getNext();
    }
}

手撸代码-单链表反转

看懂上面的代码,单链表反转其实就已经很简单了

  • 1、创建一个空的新链表,循环需要反转的链表
  • 2、先保存下次循环的数据,将循环到当前节点的下个节点设置为新链表的下个节点,然后再设置新链表下个节点设为当前循环到的节点,直到循环结束
  • 3、展示的是一个头节点插入法,还有其他方法,暂时不展示太多了(^▽^)
public static void main(String[] args) {
    OneWayLinkeList<String> linkeList =	new OneWayLinkeList<String>();
    linkeList.add("hello1");
    linkeList.add("hello2");
    linkeList.add("hello3");
    System.out.print("需要反转的链表:");
    linkeList.show();
    System.out.println("");
    System.out.println("");
    OneWayLinkeList<String> reversalLinkeList =	new OneWayLinkeList<String>();
    reversalLinkeList.reversal(linkeList.headNode);
    System.out.print("反转后的链表:");
    reversalLinkeList.show();
}

public void reversal(OneWayNode<String> node) {
    OneWayNode resultList = new OneWayNode<String>(null);
    OneWayNode p = node;
    while(p!= null){
    //保存插入点之后的数据
        OneWayNode tempList = p.getNext();
    p.setNext(resultList.getNext());
    resultList.setNext(p);
    p = tempList;
    }
    this.headNode = resultList.getNext();
}

总结

这里只是简单展示了单向、双向链表的部分基本操作,不足之处多多指教;
还有循环链表,原理都差不多,有兴趣的朋友可以自己看看。

适用场景

数据量较小,需要频繁增加,删除操作的场景