Java版基础数据结构算法 - 单链表和双向链表、顺序栈和链式栈、循环队列和链式队列

知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!

数组和链表

在大家接触链表之前,都大量的使用过数组,数组有‘1好2不好’,怎么解释呢?

1好是,数组的内存是绝对连续的,因此数组的随机访问操作非常的快,时间复杂度是 O ( 1 ) O(1) O(1),为常量时间,例如arr[20]和arr[2000]的访问花费的时间是一样的。

2不好是
a.数组在定义的时候,必须指定其大小,实际应用中,当数组元素满了以后,要进行扩容,扩容的代码虽然简单,如arr = Arrays.copyOf(arr, arr.length*2)就能够达到2倍扩容的效果,但是当数组的元素数量比较大的话,扩容需要经过开辟更大块内存,拷贝数据,GC回收原来的旧内存等步骤,其效率就比较低了,扩容花费的时间也长了。当扩容一段时间后,数组内存空间特别大,但是随着删除操作,最后只有少量的有效元素(如10000个数组元素空间,却只存储了10个有效元素),内存就被浪费了,这时候就得缩容(如ArrayList数组集合的缩容实现),其效率也比较低。

b.在数组中,插入元素或者删除元素,都需要经过大量数据的移动。插入操作会引起插入点后面的元素都向后进行移动;删除操作会引起删除点后面的元素都向前进行移动,这两个操作的时间花费都是 O ( n ) O(n) O(n),是线性时间,说明随着数组的元素数量越多,插入删除操作的效率越低。

区别于数组,那么链表就有‘1不好2好’:

2好是
a.链表的每一个元素节点都是独立new出来的,也就是说链表中所有元素节点内存并不是连续的,而是上一个节点记录了下一个节点的地址,这样内存的使用效率就非常的高,在存储数据量比较大的时候,不需要大片连续的内存空间,因此只要当前JVM可用内存足够大,链表就可以无限生成新的节点,不存在大块内存开辟,大量数据拷贝,GC回收旧内存的内存扩容问题,这一点要比数组优秀!

b.在链表中插入数据,只需要生成新的节点接入链表中就可以,不涉及其它数据节点的移动;在链表中删除数据,只需要把待删除节点从原链表中卸载下来就可以,也不涉及其它数据节点的移动,也就是数据插入和删除的时间复杂度是 O ( 1 ) O(1) O(1),常量时间,速度很快(这个指的是插入删除操作本身花费的时间,往往在链表中做插入和删除操作的时候,首先会遍历链表,找到合适的位置,那么链表搜索的时间是 O ( n ) O(n) O(n),为线性时间)。

1不好是,因为链表中节点的内存不是连续的,所以就不能像数组那样支持元素的随机访问(就是通过指定下标,访问对应的元素的值),当在链表中访问一个元素节点时,总是要从头节点开始,一个个往后遍历,链表元素越多,遍历的时间就越长,因此链表遍历搜索的时间是 O ( n ) O(n) O(n),为线性时间,没有数组随机访问 O ( 1 ) O(1) O(1)的效率好

结论:所以一般插入删除操作多,用链表;随机访问多,用数组。当然还需要具体情况具体分析。数组和链表各操作时间复杂度对比如下:

名称插入删除查找&搜搜随机访问
数组 O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)
单链表 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)不支持

单链表代码实现

/**
 * 带头节点的单链表的实现
 * @param <T>
 */
class Link<T extends Comparable<T>>{

    /**
     * 指向单链表的头节点,其地址域中记录了链表第一个节点的地址
     */
    HeadEntry<T> head;

    /**
     * 初始化链表,生成头节点被head指向
     */
    public Link(){
        this.head = new HeadEntry<>(0, null, null);
    }

    /**
     * 单链表的头插法
     * @param val
     */
    public void insertHead(T val){
        Entry<T> node = new Entry<>(val, this.head.next);
        this.head.next = node;
        this.head.cnt += 1; // 更新头节点中链表节点的个数
    }

    /**
     * 单链表的尾插法
     * @param val
     */
    public void insertTail(T val){
        Entry<T> node = head;
        while(node.next != null){
            node = node.next;
        }
        node.next = new Entry<>(val, null);
        this.head.cnt += 1; // 更新头节点中链表节点的个数
    }

    /**
     * 单链表中删除所有值是val的节点
     * @param val
     */
    public void remove(T val){
        Entry<T> pre = head;
        Entry<T> cur = head.next;

        while(cur != null){
            if(cur.data == val){
                // val节点的删除
                pre.next = cur.next;
                cur = pre.next; // 重置cur,继续向后删除链表中所有值为val的节点
                this.head.cnt -= 1; // 更新头节点中链表节点的个数
            } else {
                pre = cur;
                cur = cur.next;
            }
        }
    }

    /**
     * 打印单链表的所有节点元素的值
     */
    public void show(){
        Entry<T> cur = this.head.next;
        while (cur != null) {
            System.out.print(cur.data + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    /**
     * 获取链表节点的个数
     * @return
     */
    public int size() {
        return this.head.cnt;
    }

    /**
     * 单链表的节点类型
     * @param <T>
     */
    static class Entry<T>{
        T data; // 链表节点的数据域
        Entry<T> next; // 下一个节点的地址

        public Entry(T data, Entry<T> next) {
            this.data = data;
            this.next = next;
        }
    }

    /**
     * 头节点的类型,添加了int cnt成员变量,记录链表中节点的个数
     * @param <T>
     */
    static class HeadEntry<T> extends Entry<T>{
        int cnt; // 用来记录节点的个数

        public HeadEntry(int cnt, T data, Entry<T> next) {
            super(data, next);
            this.cnt = cnt;
        }
    }
}

/**
 * 描述: 单链表测试
 * @Author administrator
 * @Date 5/11
 */
public class LinkTestUnit {

    @Test
    public void test01(){
        Random rd = new Random();
        Link<Integer> link1 = new Link<>();

        for (int i = 0; i < 10; i++) {
            link1.insertHead(rd.nextInt(20)+1); // 随机[1,20]间的元素值
        }
        for (int i = 0; i < 10; i++) {
            link1.insertTail(rd.nextInt(20)+1); // 随机[1,20]间的元素值
        }

        link1.show();
        System.out.println(link1.size());
        link1.remove(15);
        link1.show();
        System.out.println(link1.size());
    }
}

注意:上面链表尾插法insertTail,每次插入元素都要先从头节点开始,一个个遍历到末尾节点进行添加元素,时间复杂度是 O ( n ) O(n) O(n)解决办法是可以给链表添加一个成员变量tail,让tail永远指向末尾节点,然后尾插法把新节点直接插入到tail后面,再让tail指向新的末尾节点就可以了。

链表逆置

/**
 * 逆置单链表,思想是从链表第二个节点开始采用头插法重新链接节点
 */
public void reverse(){
    if(this.head.next == null){
        return;
    }

    // 从链表第二个节点开始逆置
    Entry<T> cur = this.head.next.next;
    this.head.next.next = null;
    Entry<T> post = null;

    while(cur != null){
        post = cur.next;
        cur.next = head.next;
        head.next = cur;
        cur = post;
    }
}

寻找链表倒数第K个节点

/**
 * 获取倒数第K个单链表节点的值
 * @param k
 * @return
 */
public T getLastK(int k){
    Entry<T> cur1 = this.head.next;
    Entry<T> cur2 = this.head;

    // cur1指向第一个节点,cur2指向正数第k个节点
    for (int i = 0; i < k; i++) {
        cur2 = cur2.next;
        if(cur2 == null){
            return null;
        }
    }

    // 当cur2到达末尾节点时,cur1指向的就是倒数第k个节点
    while(cur2.next != null){
        cur1 = cur1.next;
        cur2 = cur2.next;
    }

    return cur1.data;
}

判断链表是否有环,并且找出入环节点

/**
 * 判断链表是否有环,如果有,返回入环节点的值,没有环,返回null
 * @return
 */
public T getLinkCircleVal(){
    Entry<T> slow = this.head.next;
    Entry<T> fast = this.head.next;

    // 使用快慢指针解决该问题
    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
        if(slow == fast){
            break;
        }
    }

    if(fast == null){
        return null;
    } else {
        /**
         * fast从第一个节点开始走,slow从快慢指针相交的地方开始走,
         * 它们相遇的时候,就是环的入口节点
         */
        fast = this.head.next;
        while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        return slow.data;
    }
}

判断两个单链表是否相交,并返回相交节点的值

/**
 * 判断两个单链表是否相交,思路就是先获取两个链表的长度,得到长度的差size,
 * 然后长链表先从头节点走size个,然后两个链表开始同时遍历,遇到相同的节点,说明
 * 链表相交了,如果遍历到null,说明链表没有相交
 * @param link
 * @return
 */
public T isLinkIntersect(Link<T> link){
    Entry<T> cur1 = this.head;
    Entry<T> cur2 = link.head;

    // 求两个链表的长度
    int len1 = 0;
    int len2 = 0;
    while(cur1 != null) {
        cur1 = cur1.next;
        len1++;
    }

    while(cur2 != null) {
        cur2 = cur2.next;
        len2++;
    }

    // 重置指针,找是否存在相交节点
    cur1 = this.head;
    cur2 = link.head;

    if(len1 > len2){ // 链表1比较长
        int size = len1 - len2;
        for(int i=0; i<size; ++i){
            cur1 = cur1.next;
        }
    } else if(len1 < len2){ // 链表2比较长
        int size = len2 - len1;
        for(int i=0; i<size; ++i){
            cur2 = cur2.next;
        }
    }

    while(cur1 != null && cur2 != null){
        if(cur1 == cur2){ // 两个指针指向同一个节点,链表相交了
            return cur1.data;
        }
        cur1 = cur1.next;
        cur2 = cur2.next;
    }

    return null;
}

合并两个有序单链表

/**
 * 合并两个有序的单链表
 * @param link
 */
public void merge(Link<T> link){
    Entry<T> p = this.head;
    Entry<T> p1 = this.head.next;
    Entry<T> p2 = link.head.next;

    // 比较p1和p2节点的值,把值小的节点挂在p的后面
    while(p1 != null && p2 != null){
        if(p1.data.compareTo(p2.data) >= 0){
            p.next = p2;
            p2 = p2.next;
        } else {
            p.next = p1;
            p1 = p1.next;
        }
        p = p.next;
    }

    if(p1 != null){ // 链表1还有剩余节点
        p.next = p1;
    }

    if(p2 != null){ // 链表2还有剩余节点
        p.next = p2;
    }
}

双向链表代码实现

/**
 * 双向链表的实现
 * @param <T>
 */
class Link<T extends Comparable<T>>{
    /**
     * 指向双向链表的头节点,其地址域中记录了链表第一个节点的地址
     */
    private Entry<T> head;

    /**
     * 构造函数,生成head指向的头节点,头节点不存数据
     */
    public Link(){
        head = new Entry<>(null, null, null);
    }

    /**
     * 双向链表的头插法
     * @param val
     */
    public void insertHead(T val){
        Entry<T> node = new Entry<>(val, this.head.next, this.head);
        this.head.next = node;
        if(node.next != null){
            node.next.pre = node;
        }
    }

    /**
     * 双链表的尾插法
     * @param val
     */
    public void insertTail(T val){
        Entry<T> cur = this.head;
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next = new Entry<>(val, null, cur);
    }

    /**
     * 双向链表删除所有值为val的节点
     * @param val
     */
    public void remove(T val){
        Entry<T> cur = this.head.next;
        while(cur != null){
            if(cur.data.compareTo(val) == 0){
                cur.pre.next = cur.next;
                if(cur.next != null){
                    cur.next.pre = cur.pre;
                }
            }
            cur = cur.next;
        }
    }

    /**
     * 打印双向链表所有节点元素的值
     */
    public void show(){
        Entry<T> cur = this.head.next;
        while(cur != null){
            System.out.print(cur.data + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    /**
     * 链表节点类型定义
     * @param <T>
     */
    static class Entry<T>{
        T data;
        Entry<T> next; // 存储后一个节点的地址
        Entry<T> pre; // 存储前一个节点的地址

        public Entry(T data, Entry<T> next, Entry<T> pre) {
            this.data = data;
            this.next = next;
            this.pre = pre;
        }
    }
}

/**
 * 描述:
 *
 * @Author administrator
 * @Date 5/11
 */
public class DoubleLinkTestUnit {
    public static void main(String[] args) {
        Random rand = new Random();
        Link<Integer> link = new Link<>();
        for (int i = 0; i < 10; i++) {
            link.insertHead(rand.nextInt(20) + 1);
        }
        for (int i = 0; i < 10; i++) {
            link.insertTail(rand.nextInt(20) + 1);
        }
        link.show();
        link.remove(8);
        link.show();
    }
}

栈介绍

栈是一种先进后出,后进先出的线性表数据结构,实现虽然简单,但是其用途非常的广泛,很多问题的求解都需要依赖一个栈结构,比如递归转非递归代码实现,一般都会依赖栈,比如二叉树的前中后序遍历的非递归实现;还有逆波兰表达式的求解也需要依赖栈,等等。下面总结一下Java实现的顺序栈和链栈代码。

顺序栈代码实现

/**
 * 顺序栈实现
 * @param <T>
 */
class SeqStack<T>{
    // 存储栈的元素的数组
    private T[] stack;
    // top表示栈顶的位置
    private int top;

    public SeqStack(){
        this(10);
    }

    public SeqStack(int size){
        this.stack = (T[])new Object[size];
        this.top = 0;
    }

    /**
     * 入栈操作
     * @param val
     */
    public void push(T val){
        if(full()){
            // 栈如果满了,要进行内存2倍扩容
            this.stack = Arrays.copyOf(this.stack,
                    this.stack.length*2);
        }
        this.stack[this.top] = val;
        this.top++;
    }

    /**
     * 出栈操作
     */
    public void pop(){
        if(empty())
            return;
        this.top--;
        this.stack[this.top] = null;
    }

    /**
     * 返回栈顶元素
     * @return
     */
    public T top(){
        return this.stack[this.top - 1];
    }

    /**
     * 判断栈满
     * @return
     */
    public boolean full(){
        return this.top == this.stack.length;
    }

    /**
     * 判断栈空
     * @return
     */
    public boolean empty(){
        return this.top == 0;
    }
}

链式栈代码实现

/**
 * 描述:链式栈结构
 */
public class LinkStack<T> {
    // top指向头节点,头节点的后面就是栈顶节点
    private Entry<T> top;

    public LinkStack(){
        this.top = new Entry<>(null, null);
    }

    /**
     * 入栈操作
     * @param val
     */
    public void push(T val){
        Entry<T> node = new Entry<>(val, this.top.next);
        this.top.next = node;
    }

    /**
     * 出栈操作
     * @return
     */
    public T pop(){
        T val = null;
        if(this.top.next != null){
            val = this.top.next.data;
            this.top.next = this.top.next.next;
        }
        return val;
    }

    /**
     * 查看栈顶元素
     * @return
     */
    public T peek(){
        T val = null;
        if(this.top.next != null){
            val = this.top.next.data;
        }
        return val;
    }

    /**
     * 判断栈空
     * @return
     */
    public boolean isEmpty(){
        return this.top.next == null;
    }

    /**
     * 节点类型定义
     * @param <T>
     */
    static class Entry<T>{
        T data;
        Entry<T> next;

        public Entry(T data, Entry<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}

队列介绍

队列是一种先进先出,后进后出的线性表数据结构,其用途也十分广泛,比如二叉树层序遍历非递归实现、树或者图的广度优先遍历寻找最优路径信息,都需要用到队列这种数据结构。下面总结一下Java实现的循环队列和链式队列代码。

循环队列代码实现

/**
 * 循环队列
 */
class Queue<E>{
    // 存储队列元素的数组
    private E[] que;
    // 表示队头的位置
    private int front;
    // 表示队尾的位置
    private int rear;

    /**
     * 默认构造队列,初始大小是10
     */
    public Queue(){
        this(10);
    }

    /**
     * 用户可以指定队列的大小size
     * @param size
     */
    public Queue(int size){
        this.que = (E[])new Object[size];
        this.front = this.rear = 0;
    }

    /**
     * 入队操作
     * @param val
     */
    public void offer(E val){
        if(full()){
            // 扩容
            E[] newQue = Arrays.copyOf(this.que,
                    this.que.length*2);
            int index = 0;
            for(int i=this.front;
                i != this.rear;
                i=(i+1)%this.que.length){
                newQue[index++] = this.que[i];
            }
            this.front = 0;
            this.rear = index;
            this.que = newQue;
        }
        this.que[this.rear] = val;
        this.rear = (this.rear+1)%this.que.length;
    }

    /**
     * 出队操作,并把出队的元素的值返回
     */
    public E poll(){
        if(empty()){
            return null;
        }
        E front = this.que[this.front];
        this.que[this.front] = null;
        this.front = (this.front+1)%this.que.length;
        return front;
    }

    /**
     * 查看队头元素
     * @return
     */
    public E peek(){
        if(empty()){
            return null;
        }
        return this.que[this.front];
    }

    /**
     * 判断队满
     * @return
     */
    public boolean full(){
        return (this.rear+1)%this.que.length == this.front;
    }

    /**
     * 判断队空
     * @return
     */
    public boolean empty(){
        return this.rear == this.front;
    }
}

链式队列代码实现

/**
 * 链式队列
 * front:指向的是链表的头节点
 * rear: 永远指向的是末尾节点
 * @param <T>
 */
public class LinkQueue<T>{
    // 指向头节点(队头)
    private Entry<T> front;
    // 指向尾节点(队尾)
    private Entry<T> rear;
    // 记录队列节点的个数
    private int count;

    /**
     * 初始化,front和rear都指向头节点
     */
    public LinkQueue(){
        this.front = this.rear = new Entry<>(null, null);
    }

    /**
     * 入队操作
     * @param val
     */
    public void offer(T val){
        Entry<T> node = new Entry<>(val, null);
        this.rear.next = node;
        this.rear = node;
        this.count++;
    }

    /**
     * 出队操作
     * @return
     */
    public T poll(){
        T val = null;
        if(this.front.next != null){
            val = this.front.next.data;
            this.front.next = this.front.next.next;
            // 删除队列最后一个元素,要把rear指向front,队列才能判空
            if(this.front.next == null){
                this.rear = this.front;
            }
            this.count--;
        }
        return val;
    }

    public T peek(){
        T val = null;
        if(this.front.next != null){
            val = this.front.next.data;
        }
        return val;
    }

    /**
     * 判断队列空
     * @return
     */
    public boolean isEmpty(){
        return this.front == this.rear;
    }

    /**
     * 返回队列元素的个数
     * @return
     */
    public int size(){
        return this.count;
    }

    /**
     * 节点类型定义
     * @param <T>
     */
    static class Entry<T>{
        T data;
        Entry<T> next;

        public Entry(T data, Entry<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值