上次说到动态数组,
动态数组有个很明显的缺点,就是在增加元素、删除元素时候扩容很容易造成内存空间的浪费
链表是一种链式存储的线性表,所有的元素内存地址不一定连续。
链表的常见操作有:增加元素、删除元素等。
增加元素:
在增加元素的时候,主要是找到要插入位置的前一个节点。
删除元素:
同样和是找到要删除元素的上一个节点
链表类的设计:
class LinkedList<E>{//泛型
private int size;//链表长度
private Node<E> first;//指向第一个地址
//内部类
private static class Node<E>{
E element;//存储的元素
Node<E> next;//指向下一个地址
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
//链表的长度
public int size() {
return size;
}
//链表是否为空
public boolean isEmpty() {
return size == 0;
}
//清空链表
public void clear() {
size = 0;
first = null;
}
//得到index位置上节点的值
public E get(int index) {
return node(index).element;
}
//修改index位置上节点的值
public void set(int index, E element) {
Node<E> node = node(index);
node.element = element;
}
//在指定位置上添加元素(先获取前一个节点)
public void add(int index, E element) {
rangeCheckForAdd(index);
if(index == 0) {
//相当于newNode.next = first; first = newNode
first = new Node<>(element,first);
}else {
Node<E> preNode = node(index - 1);//上一个节点
//相当于newNode.next = preNode.next; preNode.next = newNode
preNode.next = new Node<>(element,preNode.next);
}
size++;
}
//往链表的最后添加元素
public void add(E element) {
add(size,element);
}
//删除节点(先获取前一个节点)
public void remove(int index) {
if(index == 0) {
first = first.next;
}else {
Node<E> preNode = node(index - 1);
preNode.next = preNode.next.next;
}
size--;
}
//找到index位置上的节点
private Node<E> node(int index){
rangeCheck(index);//边界检查
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
//是否包含某个元素
public boolean contains(E element) {
return indexOf(element) != -1;
}
public int indexOf(E element) {
//元素可以是null时
if(element == null) {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if(node.element == element) return i;
node = node.next;
}
}else {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if(element.equals(node.element)) return i;
node = node.next;
}
}
return -1;//没有找到
}
//index越界的时候,抛出异常
private void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index: "+index+",Size :"+size);
}
//对边界的检查
private void rangeCheck(int index) {
if(index < 0 || index >= size) {
outOfBounds(index);
}
}
//添加元素时,对边界的检查
private void rangeCheckForAdd(int index) {
if(index < 0 || index > size) {
outOfBounds(index);
}
}
//打印链表
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size = ").append(size).append(",[");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if(i!=0) string.append(", ");
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
练习题:
1.删除链表的结点
.
注意:这道题只给你了要删除的节点,我们找不到它的前一个节点,这时我们可以把它的下一个节点的值赋给它,删除它的下一个节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}
2.反转链表
leetcode:https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/
这道题有两种方法:迭代法和递归法
迭代法:遍历原来的链表,然后用头插法一次插入新的链表。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newhead = null;
while(head!=null){
ListNode temp = head.next;//先用保存要插入节点的下一个节点,避免链表断开
head.next = newhead;
newhead = head;
head = temp;
}
}
}
递归法:
这个稍微有点抽象,首先要搞清楚函数的功能,它返回的是已经反转好的newhead
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newhead = ListNode reverseList(head.next);
head.next.next = head;
head.next = null;
return newhead;
}
}
3.环形链表
leetcode:https://leetcode-cn.com/problems/linked-list-cycle/
使用了一种叫快慢指针的思想,定义一个走的慢的slow指针,定义一个走的快的fast指针,如果链表中存在环形,fast指针最终会和slow指针指向同一地址,就像跑操场一样,跑得快的人,过一段时间后,会从后面追上跑得慢的人。如果fast最终出去了链表,那么就不存在环。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if(slow == fast) return true;
}
return false;
}
}
补充:
对于这个快慢指针法,还可用于这道题:https://leetcode-cn.com/problems/middle-of-the-linked-list/
用两个指针 slow与 fast
一起遍历链表。slow
一次走一步,fast
一次走两步。那么当 fast
到达链表的末尾时,slow
必然位于中间。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast!=null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}