数据结构与算法之链表
课程时间为两个半月,共二十几节课,具有连贯性,希望兄弟们尽量不要缺席。
1 回顾上节课作业:
二维数组的内存地址是怎么样的?写出寻址公式?
- 一维数组内存地址计算公式:
loc = init_loc + index * size
。 - 二维数组可转化为一维数组,如元素 4 在二维数组中位置是(1,0),转化为一维后是第 3 个元素。其在一维中的下标计算方式为:
i * n + j
(其中i
为行下标,n
为一维长度,j
为列下标)。
2 链表 面试经典
- 如何设计一个LRU缓存淘汰算法?
- 说明:最近使用淘汰算法,只需要维护一个有序的单链表就可以了。有序的指的就是加入的时间排序
- 约瑟夫问题
- 约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。
- 比如N=6,M=5 留下的就是1
- 1 2 3 4 5 6 => 6 1 2 3 4 => 6 1 2 3 =>1 2 3 => 1 3 => 1
3 链表的基本概念
- 定义:将一组零散的内存块串联在一起,通过指针来访问数据。
- 特点:不需要连续的内存空间,有指针引用,常见的有单链表、双向链表和循环链表。
- 操作:包括查找、插入和删除等,时间复杂度均为O(n)。
4 单链表
结构:由节点组成,每个节点包含数据和指向下一个节点的指针。
操作:插入操作分为头插法、尾插法和中间插入法,删除操作则是通过修改指针来实现。
链表集合示例代码
/**
* 单向链表
* 实现思路:
* 1. 节点类 MyOneWayNode
* 2. 链表类 MyOneWayLinkList
* 3. 新增节点(头插法) addToHead
* 3. 新增节点(尾插法) addToTail
* 4. 删除节点(头部) deleteToHead
* 4. 删除节点(尾部) deleteToTail
* 5. 遍历节点遍历 print
* 6. 搜索节点 search
*/
public class MyOneWayLinkList {
private MyOneWayNode head;
public MyOneWayLinkList() {
this.head = null;
}
public void print() {
MyOneWayNode cur = head;
while (cur != null){
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
public void addToHead(int data) {
MyOneWayNode node = new MyOneWayNode(data);
node.next = head;
head = node;
}
public void addToTail(int data) {
MyOneWayNode node = new MyOneWayNode(data);
if (head == null){
head = node;
}else {
MyOneWayNode cur = head;
while (cur.next != null){
cur = cur.next;
}
cur.next = node;
}
}
public MyOneWayNode deleteToHead() {
if (head != null){
MyOneWayNode node = head;
head = head.next;
return node;
}
return null;
}
public MyOneWayNode deleteToTail() {
MyOneWayNode node = null;
if (head != null){
MyOneWayNode cur = head;
while (cur.next != null && cur.next.next != null){
cur = cur.next;
}
if (cur.next != null){
node = cur.next;
cur.next = null;
}else {
node = cur;
head = null;
}
}
return node;
}
public MyOneWayNode search(int data) {
MyOneWayNode cur = head;
while (cur != null){
if (cur.data == data){
return cur;
}
cur = cur.next;
}
return null;
}
public static void main(String[] args) {
MyOneWayLinkList myOneWayLinkList = new MyOneWayLinkList();
myOneWayLinkList.addToHead(1);
myOneWayLinkList.addToHead(2);
myOneWayLinkList.addToHead(3);
System.out.println("addToHead...end...");
myOneWayLinkList.print(); // 3 2 1
myOneWayLinkList.addToTail(4);
System.out.println("addToTail...end...");
myOneWayLinkList.print(); // 3 2 1 4
myOneWayLinkList.deleteToHead();
System.out.println("deleteToHead...end...");
myOneWayLinkList.print(); // 2 1 4
myOneWayLinkList.deleteToTail();
System.out.println("deleteToTail...end...");
myOneWayLinkList.print(); // 2 1
System.out.println(myOneWayLinkList.search(2)); // MyOneWayNode{data=2, next=MyOneWayNode{data=1, next=null}}
}
}
class MyOneWayNode{
int data;
MyOneWayNode next;
public MyOneWayNode(int data) {
this.data = data;
}
@Override
public String toString() {
return "MyOneWayNode{" +
"data=" + data +
", next=" + next +
'}';
}
}
数组集合示例代码
/**
* 数组集合
* 1.属性
* 数组对象
* 集合大小
* 2.新增
* 3.删除
* 4.查找
* 5.遍历
*/
public class MyArrayList<T> {
private T[] array;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public MyArrayList() {
this.array = (T[]) new Object[DEFAULT_CAPACITY];
this.size = 0;
}
public MyArrayList(int capacity) {
this.array = (T[]) new Object[capacity];
this.size = 0;
}
public void add(T t) {
if (size == array.length) {
resize();
}
array[size++] = t;
}
public void add(int index,T t){
if (index < 0 || index >= array.length){
throw new RuntimeException("index out of bound");
}
if (size == array.length){
resize();
}
for (int i = size; i >=index; i--) {
array[i] = array[i - 1];
}
array[index] = t;
size++;
}
public void remove(int index){
if (index < 0 || index >= array.length){
throw new RuntimeException("index out of bound");
}
for (int i = index; i < size; i++) {
array[i] = array[i + 1];
}
size--;
}
public T get(int index){
if (index < 0 || index >= array.length){
throw new RuntimeException("index out of bound");
}
return array[index];
}
public void print(){
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
}
private void resize() {
T[] newArray = (T[]) new Object[array.length * 2];
for (int i = 0; i < array.length; i++) {
newArray[i] = array[i];
}
array = newArray;
}
public static void main(String[] args) {
MyArrayList<Integer> myArrayList = new MyArrayList<>();
myArrayList.add(1);
myArrayList.add(2);
myArrayList.add(3);
myArrayList.add(4);
myArrayList.add(5);
myArrayList.add(6);
myArrayList.add(7);
myArrayList.add(8);
myArrayList.add(9);
myArrayList.add(10);
System.out.println("initial......");
myArrayList.print(); // 1 2 3 4 5 6 7 8 9 10
System.out.println();
myArrayList.add(3, 100);
System.out.println("add......");
myArrayList.print(); // 1 2 3 100 4 5 6 7 8 9 10
System.out.println();
myArrayList.remove(3);
System.out.println("remove......");
myArrayList.print(); // 1 2 3 4 5 6 7 8 9 10
System.out.println();
System.out.println(myArrayList.get(3)); // 4
System.out.println(myArrayList.get(4)); // 5
}
}
5 循环链表
结构:尾节点的指针指向头节点,形成一个环形链表。
应用:解决约瑟夫问题,即围成一个圈,从第一个人开始报数,报到指定数字的人退出,直到最后一个人。
循环链表示例代码
package cn.zxc.demo.leetcode_demo.linklist;
/**
* 环形链表
* 1.环形链表节点类
* 2.环形链表类
* 3.新增节点
* 4.删除节点
* 5.打印链表
*/
public class MyRoundLinkList {
private MyRoundNode head;
private MyRoundNode tail;
public MyRoundLinkList() {
this.head = null;
this.tail = null;
}
public void print() {
MyRoundNode cur = head;
if (cur != null) System.out.print(cur.data + " -> ");
while (cur!= null && cur.next != head){
cur = cur.next;
System.out.print(cur.data + " -> ");
}
if (cur != null && cur.next != null) System.out.print(cur.next.data + " -> ");
System.out.println();
}
public void addToHead(int data) {
MyRoundNode node = new MyRoundNode(data);
node.next = head;
head = node;
if (tail == null){
tail = node;
tail.next = head;
}else {
tail.next = node;
}
}
public void addToTail(int data) {
MyRoundNode node = new MyRoundNode(data);
if (head == null){
head = node;
tail = node;
head.next = tail;
tail.next = head;
}else {
tail.next = node;
tail = node;
tail.next = head;
}
}
public MyRoundNode deleteToHead() {
MyRoundNode node = null;
if (head != null){
node = head;
if (head == tail){
tail = null;
head = null;
}else{
head = head.next;
tail.next = head;
}
}
if (node != null) node.next = null;
return node;
}
public MyRoundNode deleteToTail() {
MyRoundNode node = null;
if (head != null){
node = tail;
if (head == tail){
head = null;
tail = null;
}else{
MyRoundNode cur = head;
while (cur != null && cur.next != tail){
cur = cur.next;
}
cur.next = head;
tail.next = null; // 为了方便GC
tail = cur;
}
}
return node;
}
public MyRoundNode deleteToN(int num) {
MyRoundNode node = null;
if (head != null){
if (head == tail){ // 只有一个节点
node = tail;
head = null;
tail = null;
}else{
MyRoundNode cur = head;
for (int i = 0; i < num - 2; i++) { // 找到待删除节点的前一个节点
cur = cur.next;
}
node = cur.next; // 待删除节点
if (node == tail){ // 待删除节点 == 尾节点 更新尾节点
tail = cur;
tail.next = head;
} else if (node == head) { // 待删除节点 == 头节点 更新头节点,并更新尾部节点的下一个节点
head = node.next;
tail.next = head;
}else{
cur.next = cur.next.next; // 删除节点
}
node.next = null; // 为了方便GC
}
}
return node;
}
public MyRoundNode search(int data) {
MyRoundNode cur = head;
if (head != null && cur.data == data) return cur;
while (cur != null && cur.next != null && cur.next != head){
if (cur.next.data == data){
return cur;
}
cur = cur.next;
}
return null;
}
public static void main(String[] args) {
MyRoundLinkList myRoundLinkList = new MyRoundLinkList();
myRoundLinkList.addToHead(1);
myRoundLinkList.print(); // 1 -> 1
myRoundLinkList.addToHead(2);
myRoundLinkList.addToHead(3);
System.out.println("addToHead...end...");
myRoundLinkList.print(); // 3 -> 2 -> 1 -> 3
myRoundLinkList.addToTail(4);
System.out.println("addToTail...end...");
myRoundLinkList.print(); // 3 -> 2 -> 1 -> 4 -> 3
myRoundLinkList.deleteToHead();
System.out.println("deleteToHead...end...");
myRoundLinkList.print(); // 2 -> 1 -> 4 -> 2
myRoundLinkList.deleteToTail();
System.out.println("deleteToTail...end...");
myRoundLinkList.print(); // 2 -> 1 -> 2
System.out.println(myRoundLinkList.search(2));
}
}
class MyRoundNode{
int data;
MyRoundNode next;
public MyRoundNode(int data) {
this.data = data;
}
public MyRoundNode(int data, MyRoundNode next) {
this.data = data;
this.next = next;
}
}
解决约瑟夫问题
/**
* 解决约瑟夫问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉
* 思路:使用环形链表处理
*
*/
public class YsfDemo {
public static void main(String[] args) {
int n = 6;
int m = 5;
int[] array = initArray(n);
int num = ysf(array, m);
System.out.println("最后剩下的人:" + num);
}
private static int ysf(int[] array, int m) {
// 将数组转换为环形链表
MyRoundLinkList myRoundLinkList = new MyRoundLinkList();
for (int i = 0; i < array.length; i++) {
myRoundLinkList.addToTail(array[i]);
}
System.out.print("当前链表 : ");
myRoundLinkList.print();
MyRoundNode node = null;
MyRoundNode temp = null;
// 遍历环形链表
while ((temp = myRoundLinkList.deleteToN(m)) != null){
node = temp;
System.out.println("删除节点:" + node.data);
System.out.print("当前链表 : ");
myRoundLinkList.print();
}
return node.data;
}
private static int[] initArray(int i) {
int[] array = new int[i];
for (int j = 0; j < i; j++) {
array[j] = j + 1;
}
return array;
}
}
当前链表 : 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 1 ->
删除节点:5
当前链表 : 1 -> 2 -> 3 -> 4 -> 6 -> 1 ->
删除节点:6
当前链表 : 1 -> 2 -> 3 -> 4 -> 1 ->
删除节点:1
当前链表 : 2 -> 3 -> 4 -> 2 ->
删除节点:3
当前链表 : 2 -> 4 -> 2 ->
删除节点:2
当前链表 : 4 -> 4 ->
删除节点:4
当前链表 :
最后剩下的人:4
6 双向链表
结构:每个节点包含数据、指向前一个节点的指针和指向后一个节点的指针。
操作:插入操作需要修改两个指针,删除操作则需要修改三个指针。
示例代码
package cn.zxc.demo.leetcode_demo.linklist;
/**
* 双向链表
* 1.节点类
* 数值
* 上一个系节点
* 下一个节点
* 2.链表类
* 头插法
* 尾插法
* 删除节点
* 搜索节点
* 打印节点
*/
public class MyDoubleLinkList {
public MyDoubleLinkNode head;
public MyDoubleLinkList()
{
head = null;
}
public void addToHead(int data)
{
MyDoubleLinkNode node = new MyDoubleLinkNode(data);
if (head != null){
head.pre = node;
node.next = head;
}
head = node;
}
public void addToTail(int data)
{
MyDoubleLinkNode node = new MyDoubleLinkNode(data);
if (head == null){
head = node;
return;
}
MyDoubleLinkNode cur = head;
while (cur.next != null){
cur = cur.next;
}
cur.next = node;
node.pre = cur;
}
public void delete(int data)
{
MyDoubleLinkNode node = search(data);
if (node != null){
if (node.pre != null){ // ① 待删除节点的pre存在:将pre节点的next指向待删除节点的next
node.pre.next = node.next;
if (node.next != null) node.next.pre = node.pre;
}else{ // ② 待删除节点的pre不存在:将head指向待删除节点的next
head = node.next;
head.pre = null;
}
node.next = null;
}
}
public MyDoubleLinkNode search(int data)
{
MyDoubleLinkNode cur = head;
while (cur != null){
if (cur.data == data){
return cur;
}
cur = cur.next;
}
return null;
}
public void print()
{
MyDoubleLinkNode cur = head;
MyDoubleLinkNode tail = null;
System.out.print("链表(正序):");
while (cur != null){
System.out.print(cur.data + " -> ");
tail = cur;
cur = cur.next;
}
System.out.println();
System.out.print("链表(倒序):");
while (tail != null){
System.out.print(tail.data + " -> ");
tail = tail.pre;
}
System.out.println();
}
public static void main(String[] args)
{
MyDoubleLinkList list = new MyDoubleLinkList();
list.addToHead(1);
list.addToHead(2);
list.addToHead(3);
list.addToTail(4);
System.out.println("初始化链表:");
list.print();
list.delete(3);
System.out.println("删除3:");
list.print();
list.delete(4);
System.out.println("删除4:");
list.print();
}
}
class MyDoubleLinkNode{
int data;
MyDoubleLinkNode pre;
MyDoubleLinkNode next;
public MyDoubleLinkNode(int data)
{
this.data = data;
}
}
初始化链表:
链表(正序):3 -> 2 -> 1 -> 4 ->
链表(倒序):4 -> 1 -> 2 -> 3 ->
删除3:
链表(正序):2 -> 1 -> 4 ->
链表(倒序):4 -> 1 -> 2 ->
删除4:
链表(正序):2 -> 1 ->
链表(倒序):1 -> 2 ->
7 数组VS链表
存储方式
- 数组:数组在内存中采用连续存储的方式,即数组中的元素在内存中是连续排列的。这种存储方式使得数组在访问元素时非常高效,因为可以通过索引直接计算元素的内存地址。
- 链表:链表在内存中并不是连续存储的,而是通过指针(或引用)将各个节点连接在一起。每个节点包含两部分信息:一部分是数据域,用于存储数据;另一部分是指针域(或引用域),用于指向下一个节点。由于链表中的节点是分散存储的,因此访问链表中的元素需要通过指针进行遍历。
访问效率
- 数组:由于数组在内存中是连续存储的,因此可以通过索引直接访问数组中的元素。这种访问方式非常高效,时间复杂度为O(1)。此外,数组还可以借助CPU的缓存机制,预读数组中的数据,进一步提高访问效率。
- 链表:由于链表中的节点是分散存储的,因此访问链表中的元素需要通过指针进行遍历。这种访问方式相对较慢,时间复杂度为O(n)。此外,链表对CPU缓存不友好,因为每次访问下一个节点时都需要从内存中读取新的数据,无法有效利用缓存机制。
大小限制与动态扩容
- 数组:数组的大小在声明时是固定的,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。在数组需要扩容时,通常需要再申请一个更大的内存空间,把原数组拷贝进去,这个过程非常费时。
- 链表:链表本身没有大小的限制,天然地支持动态扩容。当需要添加新的节点时,只需要分配一个新的节点并将其插入到链表中即可。因此,链表在动态扩容方面比数组更加灵活和高效。
8 总结
- 链表是一种重要的数据结构,它可以动态地增加和删除节点,不需要预先分配连续的内存空间。
- 单链表是最简单的链表结构,它只有一个指针,只能单向遍历。
- 循环链表是一种特殊的单链表,它的尾节点指向头节点,形成一个环形结构。
- 双向链表是一种更复杂的链表结构,它有两个指针,可以双向遍历。
- 链表的应用非常广泛,例如LRU缓存淘汰算法和约瑟夫问题等。