顺序表和链表

本文详细比较了顺序表和链表在属性、方法(如头插、头删、尾插、尾删)以及增删操作的时间复杂度,特别强调了顺序表和链表在结构、空间效率和插入删除效率上的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、顺序表和链表中的属性和方法

属性顺序表单向链表双向链表
数组(elem)

节点(ListNode),

头(head)

节点(ListNode),

头(head),

尾巴(tail)

方法顺序表单向链表双向链表
打印O(n)O(n)O(n)
头插

要从0下标开始往后移数据

O(n)

O(1)O(1)
尾插O(1)

没有tail,要找尾巴

O(n)

有tail,不要找尾巴

O(1)

O(1)
index位置插

要从index下标开始往后移数据

O(n)

要找index-1位置的节点

O(n)

要找index位置的节点

O(n)

给下标,返回该下标的值O(1)O(n)O(n)
是否包含某个值O(n)O(n)O(n)
头删

移动数据

O(n)

O(1)O(1)
尾删

O(1)

找尾巴的前驱

O(n)

O(1)
删除第一次出现的某值O(n)

要定义个前驱

O(n)

O(n)

删除所有出现的某值O(n)

要定义个前驱

O(n)

O(n)
清空O(1)O(1)O(n)

2、顺序表之头插头删和尾插尾删

对于顺序表头插 头删 的时间复杂度都为O(n),因为都要移动数据

尾插 尾删 的时间复杂度都为O(1),因为不要移动数据

(1)头插:要移动所有数据,时间复杂度O(n)

public void addFirst(int data){
        if(isFull()){
            this.elem = Arrays.copyOf(this.elem,this.elem.length*2);
        }
        //把所有数据都往后移动
        for (int i = this.size-1; i >= 0; i--) {
            this.elem[i+1] = this.elem[i];
        }
        this.elem[0] = data;
        this.size++;
}

(2)头删:要移动所有数据,时间复杂度O(n)

public void removeFirst(){
        //要移动元素
        if(isEmpty()){
            return;
        }
        for (int i = 1; i < this.size; i++) {
            this.elem[i-1] = this.elem[i];
        }
        this.size--;
}

(3)尾插:时间复杂度O(1)

public void addLast(int data){
        //如果满了,要扩容,然后才能增
        if(isFull()){
            this.elem = Arrays.copyOf(this.elem,this.elem.length*2);
        }
        this.elem[this.size] = data;
        this.size++;
}

(4)尾删:时间复杂度O(1)

public void removeLast(){
        if(isEmpty()){
            return;
        }
        this.size--;
}

3、链表之头插头删和尾插尾删

(1)对于单向链表来说,

无论有没有tail,头插 头删的时间复杂度都是O(1)尾删的时间复杂度也都为O(n)【因为尾删,需要找到tail的前驱,单向链表即使有tail,也无法到达前一个节点,所以还是得遍历找tail的前驱】

没有tail尾插的时间复杂度是O(n),因为要找尾巴

有tail尾插的时间复杂度是O(1),因为不要找尾巴了

(2)对于双向链表来说,

头插 头删 尾插 尾删 的时间复杂度都为O(1)

单向链表:

没有tail:

(1)头插:时间复杂度O(1)

public void addFirst(int data){
        ListNode node = new ListNode(data);

        if(this.head == null){
            this.head = node;
            return;
        }
        //下面2句是关键代码
        node.next = head;
        head = node;
}

 (2)头删:时间复杂度O(1)

public void removeFirst(){
        if(this.head == null){
            return;
        }
        this.head = this.head.next;
}

(3)尾插:没有tail,要找尾巴,时间复杂度O(n)

//要找尾巴
public void addLast(int data){
        ListNode node = new ListNode(data);
        ListNode cur = this.head;

        if(cur == null){
            this.head = node;
            return;
        }
        //找尾巴
        while(cur.next != null){
            cur = cur.next;
        }
        //此时,cur指向尾巴节点
        cur.next = node;      
}

(4)尾删:要找尾巴节点的前驱,时间复杂度O(n)

public void removeLast(){
        if(this.head == null){
            return;
        }
        if(this.head.next == null){
            this.head = null;
            return;
        }
        //找尾巴的前驱
        ListNode prev = this.head;
        while(prev.next.next != null){
            prev = prev.next;
        }
        //到这里,prev就是尾巴节点的前一个节点
        prev.next = null;
}

有tail: 

(1)头插:时间复杂度O(1)

public void addFirst(int data){
            ListNode node = new ListNode(data);
            if(this.head == null){
                this.head = node;
                this.tail = node;
                return;
            }
            node.next = head;
            head = node;
}

(2)头删:时间复杂度O(1)

public void removeFirst(){
            if(this.head == null){
                return;
            }
            if(this.head.next == null){
                this.tail = null;
            }
            this.head = this.head.next;
}

(3)尾插:有tail,不要找尾巴,时间复杂度O(1)

 public void addLast(int data){
            ListNode node = new ListNode(data);
            if(this.tail == null){
                this.tail = node;
                this.head = node;
                return;
            }
            tail.next = node;
            tail = node;
 }

(4)尾删:要找尾巴节点的前驱,时间复杂度O(n)

public void removeLast(){
            if(this.head == null){
                return;
            }
            if(this.head.next == null){
                this.tail = null;
                this.head = null;
                return;
            }
            //找尾巴节点的前驱
            ListNode prev = this.head;
            while(prev.next != tail){
                prev = prev.next;
            }
            //到这,prev是tail的前驱
            prev.next = null;
            tail = prev;
}

双向链表:

(1)头插:时间复杂度O(1)

public void addFirst(int data){
        ListNode node = new ListNode(data);
        if(this.head == null){
            this.head = node;
            this.tail = node;
            return;
        }
        //下面3句是关键代码
        node.next = head;
        head.prev = node;
        head = node;
}

(2)头删:时间复杂度O(1)

public void removeFirst(){
        if(this.head == null){
            return;
        }
        if(this.head.next == null){
            this.head = null;
            this.tail = null;
            return;
        }
        head = head.next;
        head.prev = null;
}

(3)尾插:有tail,不要找尾巴,时间复杂度O(1)

public void addLast(int data){
        ListNode node = new ListNode(data);
        if(this.head == null){
            this.head = node;
            this.tail = node;
            return;
        }
        this.tail.next = node;
        node.prev = this.tail;
        this.tail = node;
}

(4)尾删:不要找尾巴节点的前驱,时间复杂度O(1)

public void removeLast(){
        if(this.head == null){
            return;
        }
        if(this.head.next == null){
            this.head = null;
            this.tail = null;
            return;
        }
        tail = tail.prev;
        tail.next = null;
}

4、方法之删除 

顺序表:

需要移动数据

链表:

不要移动数据,只需要修改指向

先绑定后一个节点,再绑定前一个节点,如:cur.prev.next = cur.next;先绑定后面

单向链表:

无法找到前一个节点,所以定义一个前驱prev,cur从第二个节点开始遍历

因为cur是从第二个节点开始遍历的,所以不要忘记判断头节点是不是要删除的节点。

双向链表:

可以找到前一个节点,cur从第一个节点开始遍历。

因为双向链表不仅有head,还有tail,且要修改2个指向(prev,next),所以要考虑好几种特殊情况。

  • 当删除的是第一个节点时,这个节点没有前一个节点,只要修改一个指向,head也要往后走一步
  • 当删除的是最后一个节点时,这个节点没有后一个节点,只要修改一个执行,tail也要往前走一步
  • 当删除的节点是链表中唯一一个节点时,既没有前一个节点,也没有后一个节点,不需要修改指向,但我们需要把head和tail都置空。

代码如下:

删除第一次出现的值为key的节点
和
删除所有值为key节点

单向链表: 

删除第一次出现的值为key的节点:

public void remove(int data){
        if(head == null){
            return;
        }
        if(head.val == data){
            head = head.next;
            return;
        }
        
        //直接定义一个前驱prev
        ListNode prev = this.head;
        ListNode cur = this.head.next;
        while(cur != null){
            if(cur.val == data){
                prev.next = cur.next;
                return;
            }else {
                prev = prev.next;
                cur = cur.next;
            }
        }
}

删除所有值为key节点: 

 public void removeAll(int data){
        if(head == null){
            return;
        }
        ListNode prev = this.head;
        ListNode cur = this.head.next;
        while(cur != null){
            if(cur.val == data){
                prev.next = cur.next;
                cur = cur.next;
            }else{
                prev = prev.next;
                cur = cur.next;
            }
        }
        if(head.val == data){
            head = head.next;
        }

}

双向链表:

删除第一次出现的值为key的节点:

更喜欢第一种方法!!!

cur.prev 为空,说明没有前一个节点

cur.next为空,说明没有后一个节点

cur.prev为空,且cur.next为空,说明既没有前一个节点,也没有后一个节点,即链表中只有这一个节点

刚好就是双向链表删除的几种特殊情况。

 public void remove(int data){
        ListNode cur = this.head;
        while(cur != null){
            if(cur.val == data){
                if(cur.prev == null){
                    head = head.next;
                }else{
                    cur.prev.next = cur.next;
                }
                if(cur.next == null){
                    tail = tail.next;
                }else{
                    cur.next.prev = cur.prev;
                }
                return;
            }
            cur = cur.next;
        }
}
 public void remove(int data){
        //链表为空
        if(this.head == null){
            return;
        }
        //链表只有一个节点
        if(this.head.next == null){
            if(this.head.val == data){
                this.head = null;
                this.tail = null;
            }
            return;
        }
        //删除第一个节点
        if(head.val == data){
            head = head.next;
            head.prev = null;
            return;
        }
        ListNode cur = this.head;
        while(cur != null){
            if(cur.val == data){
                cur.prev.next = cur.next;
                if(cur.next == null){
                    //删除的是最后一个节点
                    tail = tail.prev;
                }else{
                    cur.next.prev = cur.prev;
                }
                return;
            }
            cur = cur.next;
        }
}

删除所有值为key节点:

public void removeAll(int data){
        ListNode cur = this.head;
        while(cur != null){
            if(cur.val == data){
                if(cur.prev == null){
                    //删除的是第一个节点
                    head = head.next;
                }else{
                    cur.prev.next = cur.next;
                }
                if(cur.next == null){
                    //删除的是最后一个节点
                    tail = tail.prev;
                }else{
                    cur.next.prev = cur.prev;
                }
            }
            cur = cur.next;
        }
}
public void removeAll2(int data){
        ListNode cur = this.head;
        while(cur != null){
            if(cur.val == data){
                //删除的是第一个节点
                if(cur.prev == null){
                    head = head.next;
                    //如果链表只有一个节点
                    if(head == null){
                        tail = null;
                    }else {
                        head.prev = null;
                    }
                }else {
                    cur.prev.next = cur.next;
                    if (cur.next == null) {
                        //删除的是最后一个节点
                        tail = tail.prev;
                    } else {
                        cur.next.prev = cur.prev;
                    }
                }
            }
            cur = cur.next;
        }
}

5、面试题:顺序表和链表的区别是什么? 

遇到问区别的题,我们要从共性出发!!!

1、顺序表和链表都有增删查改。顺序表支持随机访问,适合给定下标查找或存储元素,时间复杂度可以达到O(1)。链表适合插入和删除比较频繁的情况。对于插入,顺序表需要移动元素,特别是头插时,时间复杂度可以达到O(N),而链表不需要移动元素,只需要修改指向。头插的时间复杂度为O(1)。单向链表增加一个尾巴节点,尾插的时间复杂度也可以达到O(1)。对于删除,链表也不需要移动元素,只需要修改指向。

2、顺序表和链表都是一种数据结构,顺序表底层是数组,是一块连续的内存,空间不够时需要扩容,扩容时可能会浪费空间。而链表是链式存储的,没有容量的概念,随用随取,不会浪费空间。

6、链表面试题(后面会每天做一题给补上)

(1)删除链表中等于给定值 val 的所有节点。(如上) 链接

public ListNode removeElements(ListNode head, int val) {
        if(head == null){
            return null;
        }
        ListNode prev = head;
        ListNode cur = head.next;
        while(cur != null){
            if(cur.val == val){
                prev.next = cur.next;
                cur = cur.next;
            }else{
                prev = cur;
                cur = cur.next;
            }
        }
        if(head.val == val){
            head = head.next;
        }
        return head;
}

(2)反转一个单链表。   链接

//头插
    public ListNode reverseList(ListNode head) {
        if(head == null){
            return null;
        }
        if(head.next == null){
            return head;
        }
        ListNode cur = head.next;
        head.next = null;
        while(cur != null){
            ListNode curNext = cur.next;
            cur.next = head;
            head = cur;
            cur = curNext;
        }
        return head;
    }

(3)给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

(4)输入一个链表,输出该链表中倒数第k个结点。

(5)将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

(6)编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。

(7)链表的回文结构。

(8)输入两个链表,找出它们的第一个公共结点。

(9)给定一个链表,判断链表中是否有环。

(10)给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值