数据结构之链表,及常见手撕链表思路及其代码实现

本文详细介绍了链表与数组的区别,包括空间利用、操作效率及适用场景。讨论了链表的优点(如动态内存管理)和缺点(如额外内存消耗和查找慢),并展示了链表在大整数相加、判断环等问题中的应用。

链表

链表和数组都是线性表,他们的不同点在于:

  1. 物理存储地址:链表的物理存储地址是不连续的,数组的物理存储地址是连续的
  2. 空间利用率:链表的空间利用率高,不会产生碎片化;数组的空间利用率低,易产生空间碎片化
  3. 开辟空间的方式:链表存储数据时,一次只开辟一个空间;而数组是“一次开辟,永久使用”,空间一旦开辟后期无法改变大小
  4. 适用场景:链表适用于写多读少的场景;数组适用于读多写少的场景

链表的优点

  • 使用链表结构,不需要提前预估长度,可以克服数组需要预先知道数据长度的缺点
  • 链表使用不连续的内存空间,可以充分利用计算机内空间,实现灵活的内存动态管理

链表的缺点

  • 链表相比于其他数组会占用更多的内存空间,因为链表中每个节点中,除了存放元素本身,还有存放指向其他节点的指针
  • 不能随机读取元素(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;
	}
}

大整数的相加

大整数相加的实现方式:

  1. BigInteger
  2. 链表

思路:用链表倒着表示十分百位,从链表的头部开始计算,若有进位则在下一个节点上加

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;
  }
}

判断链表是否存在环

思路:

  1. 基于Set集合拍段是否存在环
  2. 快慢指针判断是否存在环

方式一: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;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeMonkey-D

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值