顾名思义像线一样的性质的表,有一个打头的,有一个结尾的,中间元素一个跟个一个;
线性表List:零个或多个元素的有限序列
序列:元素之间是有顺序的,当表的长度为0称之为空表
1. 线性表的顺序存储结构
顺序存储定义:用一段地址连续的存储单元依次存储线性表的数据元素
存储方式:说白了就是在内存找一块连续的存储单元,就是一维数组
数组长度和线性表长度区别:线性表的长度(随着元素的插入删除是变化的)就是数据元素的个数 数组长度大于或等于这个
存储地址:存储器每个存储单元都有自己的编号就是内存地址,select的时候直接根据数组下标get对应的数据元素 O ( 1 ) O(1) O(1)
1.1 顺序存储结构的插入和删除
获取元素:GetElement(int index) 直接通过下标获取 时间复杂度 O ( 1 ) O(1) O(1)
插入操作:
思路:
- 如果插入位置不合适,抛出异常
- 如果线性表大于等于数组长度,则抛出异常或扩容
- 从最后一个元素开始到第i个位置的元素进行拷贝。分别将它们都向后移动一个位置
- 插入元素到i位置
- 表长加1
// 在进行第三步的时候 有个地方需要注意 记得从末尾开始往后移
public static void main(String[] args) {
int[] intArr = new int[8];
for (int i = 0; i < 6; i++) {
intArr[i] = i;
}
int i = 3;// 插队的数组下标index
System.out.println(intArr.length);
for (int k = 6; k >= i; k--) {
intArr[k + 1] = intArr[k];
}
intArr[i] = 69;
Arrays.stream(intArr).forEach(System.out::println);
}
// print result
0
1
2
69
3
4
5
0
删除元素:
- 如果删除位置不合理 抛出异常
- 删除元素
- 从删除的位置开始遍历到最后一个元素位置,分别向前移动一个位置
- 表长减一
分析插入和删除的时间复杂度:
对于最好的情况:如果元素要插入或删除最后一个位置,此时为 O ( 1 ) O(1) O(1),不需要移动其他元素
最坏的情况:如果元素查到首位 或者删除第一个元素。意味着所有的元素要移动时间复杂度为 O ( n ) O(n) O(n)
对于一般情况下,插入或删除第i个元素,取n次平均的情况: n + ( n − 1 ) + . . . + 2 + 1 n = n + 1 2 \frac{n+(n-1)+...+2+1}n=\frac{n+1}2 nn+(n−1)+...+2+1=2n+1 可以得出时间复杂度还是 O ( n ) O(n) O(n)
思考:数组的这样的线性表顺序存储结构的优缺点?
2. 线性表的链式存储结构
链式存储结构的特点:任意一组存储单元存储数据元素,不必须是一段连续的存储空间。
在顺序结构当中只需存储数据元素,现在的链式存储结构,除了需要存储数据元素以外,还需要存储它的后继元素的存储地址
这样包含数据元素和后继元素的存储地址的数据元素,称为Node 节点n个节点链接成一个链表
因为每个节点中只包含一个指针域,所以叫单链表
对于单链表,我们把链表的第一个节点叫做 头节点Head Node
// 这里自定义一个Node对象
public class ListNode {
private int val;// 存储的值
private ListNode nextNode;// 后继节点
public ListNode(int value){
this.val = value;
}
public void setNextNode(ListNode nextNode1){
this.nextNode = nextNode1;
}
public ListNode getNextNode(){
return this.nextNode;
}
public int getVal(){
return this.val;
}
}
public class ReverseNode {
// what is data structure? array is king Algorithm and data structures is program
// what is Time complexity: 时间复杂度 是一个定性描述程序运行时间
// what is Space complexity: 空间复杂度
// why the index of array is 0 start? 1: 可以节省编译时间 2: Python的作者也觉得现代语言用0开始比较优雅 3: 在支持指针的语言 比如C语言 数组的下标一般用偏移量来表示
// 实现一个链表反转?
private static final int LENGTH = 6;
public static void main(String[] args) {
ListNode head = new ListNode(1);
ListNode node1 = new ListNode(2);
ListNode node2 = new ListNode(3);
ListNode node3 = new ListNode(4);
ListNode node4 = new ListNode(5);
ListNode end = new ListNode(6);
head.setNextNode(node1);
node1.setNextNode(node2);
node2.setNextNode(node3);
node3.setNextNode(node4);
node4.setNextNode(end);
printListNode(head);
printListNode(reverseListNode(head));
printListNode(reverseListNode(end, 1, 4));
}
public static ListNode reverseListNode(ListNode head) {
if (head == null) {
return null;
}
ListNode previous = head;
ListNode current = head.getNextNode();
previous.setNextNode(null);
while (current != null) {
ListNode next = current.getNextNode();
current.setNextNode(previous);
previous = current;
current = next;
}
return previous;
}
public static ListNode reverseListNode(ListNode headNode, int m, int n) {
if (headNode == null || m >= n || n > LENGTH) {
return null;
}
// // 增加一个哨兵节点
// ListNode sentryNode = new ListNode(-1);
// sentryNode.setNextNode(headNode);
//head = sentryNode;
ListNode head = headNode;
// 保存获取第m-1个节点的node
for (int i = 0; i < m - 1; i++) {
head = head.getNextNode();
}
ListNode nodeMBefore = head;
// 获取第m个节点
head = head.getNextNode();
ListNode nodeM = head;
// 断掉m-1节点的下一个指针
//nodeMBefore.setNextNode(null);
// 获取第n个节点
for (int j = 0; j < n - m; j++) {
head = head.getNextNode();
}
ListNode nodeN = head;
// 保存第n+1个节点node
ListNode nodeNToNext = nodeN.getNextNode();
// 断掉第n个节点的下一个指针 这个地方一定要断开
nodeN.setNextNode(null);
// 调用之前反转链表的方法 返回其实就是n节点
ListNode reverseNode = reverseListNode(nodeM);
// 把m-1节点指向nodeN
nodeMBefore.setNextNode(reverseNode);
// 把nodeM指向n+1节点
nodeM.setNextNode(nodeNToNext);
return headNode;
}
public static void printListNode(ListNode headNode) {
ListNode head = headNode;
while (head != null) {
System.out.print(head.getVal());
head = head.getNextNode();
if (head != null) {
System.out.print("->");
}
}
System.out.println();
}
}
以上是一个单链表的反转demo,加深自己的理解
Leetcode的单链表深拷贝题目
2.1 单链表的读取
回顾:
我们知道在线性表的顺序存储结构中,要获取任意一个位置的元素是 O ( 1 ) O(1) O(1)
但是在单链表中,获取第i个元素必须从头开始找,相对比较麻烦,思路:
- 声明一个结点P指向链表第一个结点,初始化j从1开始
- 当j<i&&P!=null,遍历链表,让P指针向后移动指向下一个结点,j累加1
- 在遍历时候考虑是否到链表末尾 加上判断该结点是否为空
- 查找成功 返回结点P的数据元素
这个算法取决于i的位置,每次从头开始找,因此最坏情况下的时间复杂度是 O ( n ) O(n) O(n)。
2.2 单链表的插入和删除
注意插入的顺序一定不能换,思考一下什么是掉链?
// 这两句代码的顺序不可改变 一定要先获取结点的nextNode 赋值给插入结点的nextNode
s.setNextNode(p.getNextNode());
p.setNextNode(s);
3. 线性表的两种存储结构的优缺点
存储分配方式 | 时间复杂度 | 空间性能 | |
---|---|---|---|
顺序存储结构-数组 | 一段连续的存储空间 | 查找: O ( 1 ) O(1) O(1) 插入和删除: O ( n ) O(n) O(n) | 需要预分配存储空间 |
链式存储结构-单链表 | 用一组任意的存储单元 | 查找: O ( n ) O(n) O(n) 插入和删除: O ( 1 ) O(1) O(1) | 不受限制 灵活分配 |
思考:ArrayList or LinkList how to choose?
4. 循环链表
将单链表的尾结点的指针指向头结点,使得整个链表形成一个环,这种头尾相连的单链表简称为 循环链表
解决的问题:从任意一个结点开始 能够访问到链表当中的任意结点
循环条件的差异:单链表判断下一个是否是null;循环链表判断下一个是否是头结点即可
思考:怎么合并两个循环链表?
5.双向链表
为了克服单向链表单向性这个缺点,无法快速获取上一个结点的数据元素
O
(
n
)
O(n)
O(n);双向链表double linked list就闪亮登场。
在单链表的基础上,增加一个指向前驱结点的指针;
// 如果设计双向链表的对象
public class DoubleLinkedList {
private Integer value;// 结点值
private DoubleLinkedList previousNode;// 前驱结点
private DoubleLinkedList nextNode;// 后继结点
public DoubleLinkedList(Integer value, DoubleLinkedList previousNode, DoubleLinkedList nextNode) {
this.value = value;
this.previousNode = previousNode;
this.nextNode = nextNode;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
public DoubleLinkedList getPreviousNode() {
return previousNode;
}
public void setPreviousNode(DoubleLinkedList previousNode) {
this.previousNode = previousNode;
}
public DoubleLinkedList getNextNode() {
return nextNode;
}
public void setNextNode(DoubleLinkedList nextNode) {
this.nextNode = nextNode;
}
}