链表
链表和数组都是线性表,他们的不同点在于:
- 物理存储地址:链表的物理存储地址是不连续的,数组的物理存储地址是连续的
- 空间利用率:链表的空间利用率高,不会产生碎片化;数组的空间利用率低,易产生空间碎片化
- 开辟空间的方式:链表存储数据时,一次只开辟一个空间;而数组是“一次开辟,永久使用”,空间一旦开辟后期无法改变大小
- 适用场景:链表适用于写多读少的场景;数组适用于读多写少的场景
链表的优点
- 使用链表结构,不需要提前预估长度,可以克服数组需要预先知道数据长度的缺点
- 链表使用不连续的内存空间,可以充分利用计算机内空间,实现灵活的内存动态管理
链表的缺点
- 链表相比于其他数组会占用更多的内存空间,因为链表中每个节点中,除了存放元素本身,还有存放指向其他节点的指针
- 不能随机读取元素(
RandomAccess) - 遍历和查找元素比较慢
时间复杂度
- 链表的插入和删除操作的复杂度为O(1):只需要知道目标位置元素的上一个元素即可
- 查找一个节点或者访问特定位置的节点时的复杂度为O(n):最坏情况下需要遍历链表
链表的分类
- 单向链表
- 双向链表
- 循环链表
- 双向循环链表
常见手撕链表
手写单向链表
package linked;
public class Linked {
// 链表的头节点
Node first;
// 链表的尾节点
Node last;
// 链表的节点个数
private int size;
// 重写toString方法遍历单向链表
@Override
public String toString() {
StringBuilder res = new StringBuilder();
for(Node x = first; x!=null ; x= x.next) {
res.append(x.val);
if(x.next != null) {
res.append("->");
}
}
return res.toString();
}
// 采用尾插法插入元素
public void add(int val) {
final Node l = last;
Node newNode = new Node(val);
if(l == null) {
first = newNode;
}else {
l.next = newNode;
}
// 尾节点都变为新的节点
last = newNode;
size ++;
}
public int size() {
return size;
}
static class Node{
// 数据域
int val;
// 指针域
Node next;
// 构造方法
public Node(int x){
this.val = x;
}
}
}
Test:
package linked;
public class Test01 {
public static void main(String[] args) {
Linked linked = new Linked();
linked.add(1);
linked.add(2);
linked.add(3);
linked.add(4);
linked.add(5);
System.out.println("链表的长度是:"+linked.size());
System.out.println("链表的内容是:"+linked.toString());
}
}
合并并输出有序链表
实现思路:双指针法实现合并有序链表
package linked;
import linked.Linked.Node;
//合并有序链表
public class Test02 {
public static void main(String[] args) {
Linked linked1 = new Linked();
linked1.add(1);
linked1.add(3);
linked1.add(5);
linked1.add(7);
linked1.add(9);
Linked linked2 = new Linked();
linked2.add(2);
linked2.add(4);
linked2.add(6);
Linked res = meger(linked1,linked2);
System.out.println(res.toString());
}
// 双指针法实现合并有序链表
public static Linked meger(Linked l1,Linked l2) {
Linked resLinked = new Linked();
// 链表的头节点
Node p1 = l1.first;
Node p2 = l2.first;
while(p1 != null || p2 != null ) {
// 链表1为空时,链表2的指针移动
if(p1 == null) {
resLinked.add(p2.val);
p2 = p2.next;
continue;
}
// 链表2为空时,链表1的指针移动
if(p2 == null) {
resLinked.add(p1.val);
p1 = p1.next;
continue;
}
if(p1.val < p2.val) {
// 链表1的值小于链表2 ,链表1的指针移动
resLinked.add(p1.val);
p1 = p1.next;
}else {
// 链表2的值小于链表1 ,链表2的指针移动
resLinked.add(p2.val);
p2 = p2.next;
}
}
return resLinked;
}
}
链表逆序
实现思路:基于栈
package linked;
import java.util.Stack;
import linked.Linked.Node;
// 逆序链表
public class Test03 {
public static void main(String[] args) {
Linked linked = new Linked();
linked.add(1);
linked.add(2);
linked.add(3);
linked.add(4);
linked.add(5);
linked = reveseLinked(linked);
System.out.println(linked.toString());
}
// 基于栈逆序链表
public static Linked reveseLinked(Linked l1) {
// 栈
Stack<Node> stack = new Stack<Node>();
// 遍历链表
Node n = l1.first;
if(n ==null) {
return l1;
}
while( n != null) {
stack.push(n);
n = n.next;
}
// 清空链表
l1 = new Linked();
// 遍历栈
while(!stack.isEmpty()) {
Node pop = stack.pop();
l1.add(pop.val);
}
return l1;
}
}
大整数的相加
大整数相加的实现方式:
- BigInteger
- 链表
思路:用链表倒着表示十分百位,从链表的头部开始计算,若有进位则在下一个节点上加
package linked;
import linked.Linked.Node;
// 使用链表实现大整数相加
public class Test04 {
public static void main(String[] args) {
Linked number1 = new Linked();
// 753
number1.add(3);
number1.add(5);
number1.add(7);
// 634
Linked number2 = new Linked();
number2.add(4);
number2.add(3);
number2.add(6);
Linked res = addNumberToSum(number1, number2);
System.out.println(res.toString());
}
public static Linked addNumberToSum(Linked l1,Linked l2) {
// 存放结果的链表
Linked res = new Linked();
// 链表的头部 -- 数字的个位
Node n1 = l1.first;
Node n2 = l2.first;
// 进位数
int carry = 0;
while(n1 !=null || n2 != null) {
int x = n1 != null ? n1.val : 0;
int y = n2 != null ? n2.val : 0;
int sum = x + y + carry;
carry = sum / 10;
res.add(sum%10);
if(n1 != null) {
n1 = n1.next;
}
if(n2 != null) {
n2 = n2.next;
}
}
// 如果carry!=0 则说明进位,需要存入链表中
if(carry != 0) {
res.add(carry);
}
return res;
}
}
判断链表是否存在环
思路:
- 基于Set集合拍段是否存在环
- 快慢指针判断是否存在环
方式一:Set集合
public class Test05 {
public static void main(String[] args) {
Node node1 = new Linked.Node(1);
Node node2 = new Linked.Node(2);
Node node3 = new Linked.Node(3);
Node node4 = new Linked.Node(4);
Node node5 = new Linked.Node(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = node3;
//环,node3、node4、node5形成环
System.out.println(hasCycle1(node1));
}
// 基于Set集合判断是否存在环
public static boolean hasCycle1(Node node) {
// 利用Set的唯一性,来判断是否存在环
HashSet<Node> set = new HashSet<Node>();
while(node != null) {
if(set.contains(node)) {
return true;
}
set.add(node);
node = node.next;
}
return false;
}
}
方式二:基于快慢指针
快指针每次挪动两个节点
慢指针每次挪动一个节点
如果存在环,快指针则会先进入到环,等慢指针进入环时,则快指针一定就在某一次循环与慢指针指向同一地址,如果不存在环,则快指针很快就会到尾节点,最后到null,慢指针也会慢慢走到尾节点直至为null,不会指向同一地址
package linked;
import java.util.HashSet;
import linked.Linked.Node;
public class Test05 {
public static void main(String[] args) {
Node node1 = new Linked.Node(1);
Node node2 = new Linked.Node(2);
Node node3 = new Linked.Node(3);
Node node4 = new Linked.Node(4);
Node node5 = new Linked.Node(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = node3; // 存在环
System.out.println(hasCycle2(node1));
}
// “快慢指针”实现判断是否存在环
public static boolean hasCycle2(Node node) {
Node fast = node;
Node slow = node;
while(fast != null && fast.next !=null && slow !=null) {
fast = fast.next.next;
slow = slow.next;
// 判断快慢指针是否指向同一地址
if(fast == slow) {
return true;
}
}
return false;
}
}
本文详细介绍了链表与数组的区别,包括空间利用、操作效率及适用场景。讨论了链表的优点(如动态内存管理)和缺点(如额外内存消耗和查找慢),并展示了链表在大整数相加、判断环等问题中的应用。

5万+

被折叠的 条评论
为什么被折叠?



