数据结构和算法(二)–单向(循环)链表(LinkedList)
动态数组的缺点
- 扩容和缩容时,会循环遍历所有元素,当数据量很大时,此操作会有一些效率问题
- 再扩容和缩容之前会预留一部分空间,此空间是不存储内容的,而且数组长度越大预留的空间就会越多(按比例),会造成内存浪费
链表(是一种链式存储的线性表,内存地址不一定连续)
- 链表本身有一个内部类表示一个一个的节点(Node),链表本身不存储数据,只是指向头节点
- 节点是存储数据内容的真正的类,同时它也会指向下一个节点
- 链表在进行添加操作时,会新建一个Node类来存储数据,并指向下一个节点,因此每一个节点的内存地址不一定是连续的
- 通过控制节点的指针可以实现删除,改变顺序,增加等操作
链表方法设计思路
- getNode(int index)
传入index,遍历此链表,返回一个node节点,这个方法是为了在链表内部可以快速找到任何index处的节点对象 - add(E element)
调用add(E element,int index)方法,使用参数新建一个node节点获取到最后一个节点,并使最后一个节点的next指向新建的节点 - remove(int index)
找到index-1处的节点,并使其next指向index+1处的节点,注意index==0时要特殊处理 - set(int index,E element)
找到index处的节点,设置其element属性为传入的参数 - indexOf(E element)
循环遍历每一个节点,使用equals判断,当传入是null时需要特殊处理 - clear()
firstNode设置为null,size设置为0
代码实现(配合上面设计思路观看)
public class CustomLinkedList<E> implements CustomList<E> {
private Node<E> firstNode;
private int size;
public CustomLinkedList() {
size = 0;
firstNode = null;
}
private static class Node<E> {
E element;
Node<E> next;
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}
@Override
public void add(E element) {
add(size,element);
}
@Override
public E get(int index) {
Node<E> node = getNode(index);
return node.element;
}
@Override
public E set(int index, E element) {
E oldObj = get(index);
getNode(index).element = element;
return oldObj;
}
@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new RuntimeException("索引越界错误");
}
if (index == 0){
firstNode = new Node<E>(element, firstNode);
}else {
getNode(index - 1).next = new Node<E>(element, getNode(index - 1).next);
}
size++;
}
@Override
public E remove(int index) {
Object oldObj = get(index);
if (index == 0){
firstNode = firstNode.next;
}else {
getNode(index - 1).next = getNode(index).next;
}
size--;
return (E) oldObj;
}
@Override
public int indexOf(E element) {
Node<E> node = this.firstNode;
for (int i = 0; i < size; i++) {
if (element == node.element){
return i;
}else if (element != null && element.equals(node.element)) {
return i;
}
node = node.next;
}
return -1;
}
@Override
public void clear() {
size = 0;
firstNode = null;
}
private Node<E> getNode(int index) {
checkIndex(index);
Node<E> node = this.firstNode;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
private void checkIndex(int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("索引越界错误");
}
}
@Override
public String toString() {
Node<E> node = this.firstNode;
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < size; i++) {
if (i == 0) {
stringBuilder.append("[");
} else {
stringBuilder.append(",");
}
stringBuilder.append(node.element);
node = node.next;
}
stringBuilder.append("]").append(" ==> 链表长度为:").append(size);
return stringBuilder.toString();
}
}
时间复杂度分析
- add(int index, E element)
- 最大时间复杂度:O(n)
- 最小时间复杂度:O(1)
- 平均时间复杂度:O(n)
- get(int index)
- 最大时间复杂度:O(n)
- 最小时间复杂度:O(1)
- 平均时间复杂度:O(n)
- set(int index, E element)
- 最大时间复杂度:O(n)
- 最小时间复杂度:O(1)
- 平均时间复杂度:O(n)
- remove(int index)
- 最大时间复杂度:O(n)
- 最小时间复杂度:O(1)
- 平均时间复杂度:O(n)
- indexOf(E element)
- 最大时间复杂度:O(n)
- 最小时间复杂度:O(1)
- 平均时间复杂度:O(n)
链表改造(不推荐)
- 增加虚拟头节点,可以统一处理节点逻辑
练习
1. 传入一个节点对象,删除一个节点
- 链接:https://leetcode-cn.com/problems/delete-node-in-a-linked-list/
- 思路:获取传入节点的下一个节点的element,覆盖掉传入节点的element,并使传入节点的next指向下一个节点的下一个节点
- 代码实现:
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}
2. 反转链表
- 链接:https://leetcode-cn.com/problems/reverse-linked-list/
- 思路1:递归执行,先保存head的值,后面的值往前一个节点移动,再把最后一个节点的值变为保存的值
class Solution {
public ListNode reverseList(ListNode head) {
ListNode fir = head;
if(head != null){
if(head.next != null){
reverseList(head.next);
}
int a = head.val;
while (head.next != null){
head.val = head.next.val;
head = head.next;
}
head.val = a;
}
return fir;
}
}
- 思路2:递归执行,变换指针,把节点看成一个一个的个体,别管执行过程中一个节点有多个指针指向的问题,关键是最后返回一个newHead(如果想返回原来的head会变得更复杂)
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next==null)return head;
ListNode node = reverseList(head.next);
head.next.next = head;
head.next = null;
return node;
}
}
- 思路3:迭代执行,创建临时遍历存储head的下一个节点,遍历每一个节点,先使当前节点的next指向newHead,再让newHead指向当前节点,再把当前节点指向零时存储的下一个节点
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode temp = null;
ListNode now = head;
ListNode newHead = null;
while (now !=null ){
temp = now.next;
now.next = newHead;
newHead = now;
now = temp;
}
return newHead;
}
}
3. 判断链表是否有环
- 链接:https://leetcode-cn.com/problems/linked-list-cycle/
- 思路:快慢指针
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
ListNode fast = head.next;
ListNode low = head;
while (fast != null) {
low = low.next;
if(fast.next == null){
return false;
}
fast = fast.next.next;
if (low == fast){
return true;
}
}
return false;
}
}
单向循环链表
- 单向循环链表和单向链表的区别就是,在尾节点的next原本指向的是null,现在指向的是firstNode
循环链表实现思路分析
- 其中大部分方法和单向链表相同,只有增加或者删除的时候需要注意设置最后一个节点的next属性
- add(int index,E element)
往index==0处添加节点时需要注意,先把最后一个节点找出来,然后添加新节点到0处,firstNode指向它,再把之前的最后一个节点指向firstNode节点。其中需要注意的是,要先查询出最后一个节点才能改变firstNode的值。 - remove(int index)
对index==0处的节点进行删除操作时,先把最后一个节点查出来,然后把firstNode指向下一个节点,再把最后一个节点指向现在的firstNode。注意,查询最后一个节点必须要再移动firstNode之前,因为查询是传入的是size-1,而移动了firstNode之后size没有马上-1从而造成索引越界。
代码实现(重点看add和remove方法和原来的有什么区别)
public class CustomCircleLinkedList<E> implements CustomList<E>{
private Node<E> firstNode;
private int size;
public CustomCircleLinkedList() {
size = 0;
firstNode = null;
}
private static class Node<E> {
E element;
Node<E> next;
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"element=" + element +
", next=" + next.element +
'}';
}
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}
@Override
public void add(E element) {
add(size,element);
}
@Override
public E get(int index) {
Node<E> node = getNode(index);
return node.element;
}
@Override
public E set(int index, E element) {
E oldObj = get(index);
getNode(index).element = element;
return oldObj;
}
@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new RuntimeException("索引越界错误");
}
if (index == 0){
if (size ==0){
firstNode = new Node<E>(element, firstNode);
firstNode.next = firstNode;
}else {
Node<E> node = getNode(size - 1);
firstNode = new Node<E>(element, firstNode);
node.next = firstNode;
}
}else {
getNode(index - 1).next = new Node<E>(element, getNode(index - 1).next);
}
size++;
}
@Override
public E remove(int index) {
Object oldObj = get(index);
if (index == 0){
Node<E> node = getNode(size - 1);
firstNode = firstNode.next;
node.next = firstNode;
}else {
getNode(index - 1).next = getNode(index).next;
}
size--;
return (E) oldObj;
}
@Override
public int indexOf(E element) {
Node<E> node = this.firstNode;
for (int i = 0; i < size; i++) {
if (element == node.element){
return i;
}else if (element != null && element.equals(node.element)) {
return i;
}
node = node.next;
}
return -1;
}
@Override
public void clear() {
size = 0;
firstNode = null;
}
private Node<E> getNode(int index) {
checkIndex(index);
Node<E> node = this.firstNode;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
private void checkIndex(int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("索引越界错误");
}
}
@Override
public String toString() {
Node<E> node = this.firstNode;
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < size; i++) {
if (i == 0) {
stringBuilder.append("[");
} else {
stringBuilder.append(",");
}
stringBuilder.append(node);
node = node.next;
}
stringBuilder.append("]").append(" ==> 链表长度为:").append(size);
return stringBuilder.toString();
}
}