结构:数据元素之间的关系;
数据结构:相互之间存在的一种或多种特定关系的数据元素的集合。包括三方面的内容:逻辑结构、存储结构(物理结构)和数据的运算。逻辑结构和存储结构密不可分,前者影响算法的设计,后者影响算法的实现。
逻辑结构:元素之间的逻辑关系。
数据的逻辑结构分为:线性结构和非线性结构。
集合:结构中的元素的关系只是“同属于一个集合”,别无其他关系;
线性表:数据元素之间只存在一对一的关系;
树形结构:数据元素之间存在一对多的关系;
图状结构:数据元素之间存在多对多的关系;
存储结构/物理结构:是数据结构在计算机中的表示;是逻辑结构用计算机语言的实现;包括数据元素的表示和关系的表示;
数据的存储结构分为:顺序存储、链式存储、索引存储和散列存储。
备注2022/8/15
在数据结构中,从存储上可以将它分为 顺序结构 和 非顺序结构。
顺序存储 = 存储的物理位置相邻;链式存储 = 存储的物理位置未必相邻,通过记录相邻元素的物理位置找到相邻元素;
索引存储 = 类似目录 ; 散列存储 = 通过关键字直接计算出元素的物理地址。
算法的五个特性: 有穷性、确定性、可行性、输入、输出
概念
用来衡量算法随着问题规模增大,执行时间增长的快慢;
注意
符号表示
时间复杂度有最差、平均、最佳三种情况。
案例:一个长度为N的数组,判断数组中是否有7,有返回true,没有返回false。
时间复杂度分析:
时间复杂度 | 分析 |
---|---|
最佳 | nums = {7,a,b,c…} 复杂度是1 |
最差 | nums = {a,b,c,d…,n-1,7} 复杂度是n |
平均 | 需要考虑输入数据的分布情况,计算所有数据情况下的平均时间复杂度 |
复杂度 | 说明 |
---|---|
常数O(1) | 运行次数与N大小呈常量关系,不随输入数据大小N的变化而变化 |
对数O(logN) | 对数阶=每轮排除一半的情况,出现于二分法、分治等算法中 |
线性O(N) | 循环运行次数与N大小呈线性关系,常出现于遍历法算法中 |
线性对数O(NlogN) | 两层循环相互独立,第一层和第二层时间复杂度分别是O(logN) 和 O(N),总体时间复杂度为O(NlogN) |
平方O(N2) | 两层循环相互独立,都与N呈线性关系 |
指数O(2N) | 指数阶常出现于递归 |
阶乘O(N!) | 阶乘对应数学上常见的**“全排列”** |
注意
二分法 时间复杂度是logN的说明
超时
即程序运行超过了规定的时间,一般OJ(Online judge)的超时时间是1s,也就是用例数据输入后最多要1s内得到结果;
如果写一个O(n)的算法,可以估算出来多大的时候算法的执行时间就会超过1s。
如果n的规模已经足够让O(n)的算法运行时间超过了1s,那么就该考虑log(n)的算法。
概述
用来衡量算法随着问题规模增大,算法所需空间增长的快慢;
空间复杂度设计的空间类型介绍如下:
类型 | 介绍 |
---|---|
输入空间 | 存储输入数据所需的空间大小 |
暂存空间 | 算法运行过程中,存储所有中间变量和对象等数据所需的空间大小 |
输出空间 | 算法运行返回时,存储输出数据所需的空间大小 |
一般情况下算法运行计算的空间复杂度,计算所需内容是:暂存空间、输出空间。
表示
空间复杂度统计算法在“最差情况”下使用的空间大小。
最差情况有两层含义:最差输入数据、算法运行中的最差运行点。
最差输入数据。当N<= 10 数组nums的长度恒定为10,空间复杂度为O(10) = O(1);当N>10,数组nums长度为N,空间复杂度为O(N)。则空间复杂度应为最差输入数据情况下的O(N)。
最差运行点。执行nums = [0] * 10是,算法仅使用O(1)大小的空间;执行nums = [0] * N 时,算法使用O(N)的空间;那么空间复杂度应为最差运行点的O(N)。
复杂度 | 说明 |
---|---|
常数O(1) | 普通常量、变量、对象、元素数量与输入数据大小N无关的集合,皆使用常数大小的空间 |
对数O(logN) | 对数阶常出现于分治算法的栈帧空间累计、数据类型转换等 快速排序,数字转化为字符串 |
线性O(N) | 元素数量与N呈线性无关的任意类型集合 常见于以为数组、链表、哈希表等 |
平方O(N2) | 元素数量与N呈平方关系 常见于矩阵,递归调用阶段 |
指数O(2N) | 指数阶常见于二叉树、多叉树。 |
时间复杂度 和 空间复杂度 常用的表示是:o(1), o(n),o(n^2), o(logn), o(nlogn)
其中n表示数据量,详细解释如下:
符号 | 说明 | 常见算法 |
---|---|---|
o(1) | 表示数据量增加,时间、空间复杂度不受影响 | 哈希算法 |
o(n) | 表示数据量几倍,耗时、内存空间也增加几倍 | 遍历算法 动态规划 |
o(n^2) | 表示数据增加几倍,耗时、内存空间增加n的平方倍 | 冒泡排序 |
o(logn) | 表示数据增加几倍,耗时、内存增加logn倍(即数据增加25倍,耗时、存储空间增加5倍) | 二分查找 |
o(nlogn) | 表示数据增加几倍,耗时、内存增加nlogn倍(即数据量增加25倍,耗时、存储空间增加25*5=125倍) | 归并排序 |
区别 | 线性表 | 链表 |
---|---|---|
空间开辟 | 连续开辟一段空间,大小固定 | 一次只开辟一个节点的空间(单链表),空间大小是动态的 |
空间使用 | 不知道要存储多少元素,如果开辟内存太大,会造成空间浪费 知道存储元素所少,顺序表中每个元素的存储密度是1,完全不会有内存空间的浪费 |
不知道存储元素个数,存储一个元素开辟一个空间,浪费不那么严重 知道存储元素个数,每个结点都会有非数据项指针,就会造成内存浪费 编译器会为每个程序从内存上分配一段空间,给该程序使用,每次开辟空间时都是在随机的位置开辟的,使用单链表就会把该空间块弄的七零八碎,出现一些是用不到的碎片空间。 |
对CPU高速缓存的影响 | 连续开辟一段空间,可以一次把多个数据写入高速缓存,再写入主内存;线性表的 CPU高速缓存效率更高 | 单链表每需要存储一个数据就开辟一个空间,每个数据存储时都要单独写入高速缓存区,再写入主内存 ;单链表的CPU高速缓存效率更低 |
访问元素的时间复杂度 | 类似数组,通过下标访问元素,支持随机访问。时间复杂度O(1) | 不支持随机访问,只能从头结点开始遍历整个链表。时间复杂度O(n) |
随机位置插入、删除元素的时间复杂度 | 连续存储,插入、删除元素需要把它之后的元素全部后移或前移,时间开销很大 ; O(n) | 插入或删除,只需要改变它的前驱元素及插入 或 删除元素的指向即可。O(1) |
定义
具有相同数据类型的n个数据元素的有限序列(n大于0);
区分线性表、顺序表、链式表
线性表 = 逻辑结构,表示元素之间一对一的关系;
顺序表 和 链式表 = 存储结构。
特点
表中元素的逻辑顺序与其物理顺序相同;
最主要特点是可以进行随机访问,即通过首地址和元素序号可以在O(1)时间内找到指定元素;
存储密度高,每个结点只存储数据元素;
逻辑上相邻的元素物理上也相邻,所以插入和删除需要移动大量的元素;
时间复杂度
线性表插入的时间复杂度是O(n)
线性表删除的时间复杂度是O(n)
线性表按值查找的时间复杂度是O(n)
增删快、查询慢的模型
public class ListNode { // 用类来做一个节点
int val; // 设置一个成员变量
ListNode next; // 使用类来做指针,指向下一个
ListNode(int x) { val = x; }
// 带参构造方法,由于两个参数的名字不同,所以不同使用this.参数名 表示当前对象的成员
}
java中没有指针的概念,只有引用数据类型,所以使用一个节点来充当指针。
概念
单链表 = 线性表的链式存储,是通过一组任意的存储单元来存储线性表中的数据元素。
为了建立数据与元素之间的线性关系,每个链表节点包含两部分:data和next。data为数据域,存放数据元素;next为指针域,存放其后继节点的地址。
头结点:
在单链表的第一个节点之前附加一个节点,称为头结点。
头结点的数据域可以不设任何信息,也可以记录表长等相关信息。
头结点的指针域指向线性表的第一个元素节点。
时间复杂度O(n)
头插法建立单链表每个节点插入的时间为O(1),设单链表元素为n,则总的时间复杂度为O(n)
尾插发建立单链表每个节点插入的时间为O(1),设单链表元素为n,则总的时间复杂度为O(n)
按序号查找节点值的时间复杂度为O(n)
按值查找表节点的时间复杂度为O(n)
插入节点的时间复杂度为O(n),若是在给定的节点后面插入新节点,则时间复杂度仅为O(1)
删除节点的时间复杂度为O(n)
求表长的时间复杂度为O(n)
概念
结点结构包括三个部分:data数据、next指向后一个元素指针、pre指向前一个元素指针;
性质
不带头结点、带尾指针tail、带头指针head;
tail的next = null、head的pre = null;
操作
链表主要的操作还是增删。增删需要考虑是否带头节点,头插、尾插、中间插等问题。
// 节点
class node<T> {
T data;
node<T> pre;
node<T> next;
public node() {
}
public node(T data) {
this.data = data;
}
}
// 链表
public class doubleList<T> {
private node<T> head;// 头节点
private node<T> tail;// 尾节点
private int length;
//各种方法
}
双链表初始化
head始终指向第一个真实有效的数据,tail也是这样。
public doubleList(){
head = null;
tail = head;
length = 0;
}
增加=空表
// 创建一个节点
node<T> teamNode = new node(data);
if (isEmpty()) { // 对于空表而言,head和tail都需要指向真实数据,直接赋值即可。
head = teamNode;
tail = teamNode;
}
增加 = 头插
node<T> teamNode = new node(data);//创建要插入的新节点
head.pre = teamNode; // 头结点的pre = 新节点
teamNode.next = head; // 新节点的next = 头结点
teamNode.pre = null; // 新节点的pre = null
head = teamNode; // 重新定义头结点
增加 = 尾插
node<T> teamNode = new node(data); //创建要插入的新节点
tail.next = teamNdoe; // 尾结点的next = 新节点
teamNode.pre = tail; // 新节点的pre = 尾结点
teamNode.next = null; // 新节点的next = null
tail = teamNode; // 重新定义尾结点
增加 = 中间插入
node<T> teamNode = new node(data); // 创建要插入的新节点
node<T> preNode; // 插入位置
teamNode.pre = preNode; // 新节点的pre = preNode
teamNode.next = preNode.next; // 新节点的next = preNode.next
preNode.next = teamNode;
删除 = 单点删除
// 链表中只有一个元素 无论头插还是尾插,直接将链表初始化就好
if(length = 1){
head = null;
tail = head;
length = 0;
}
删除 = 头删
node<T> newHead = head.next; // 获取头结点的next元素
newHead.pre = null; // 将该节点的pre设置为null
head = newHead; // 重新定义头结点
删除 = 尾删
node<T> newTail = tail.pre; // 获取尾结点的pre元素
newTail.next = null; // 新尾结点的next = null
tail = newTail; // 重新定义尾结点
删除 = 中间
node<T> deleteNode ; // 要删除的结点
noet<T> preNode = deleteNode.pre; // 要删节点的前节点
node<T> nextNode = deleteNode.next; // 要删节点的后节点
preNode.next = nextNode; // 前节点的next = 后节点
nextNode.pre = preNode; // 后节点的pre = 前节点
//简化
deleteNode.next.pre = deleteNode.pre;
deleteNode.pre.next = deleteNode.next;
概念
与单链表区别:
区别 | 单链表 | 循环单链表 |
---|---|---|
结构 | 最后一个元素的next 是null | 最后一个元素的next不是null,而是指向head节点 |
结束标志 | 节点的next = null | 没有,非要找到结束标志的话就是元素的next节点指向head |
遍历方式 | 每次遍历都得从head节点开始 | 可以从任意节点开始 |
操作方式 | 对head节点较为关注 | 不关注head节点,带来极大便利 |
应用场景 | 传统队列特性的数据处理(排队候车) | 具有环状特性的数据处理(约瑟夫问题) |
特点
循环单链表基本结构
public class CircularLinkList<E> {
public static Random RANDOM = new Random();
public MyNode<E> head; // 头结点
public int size = 0; // 链表长度
public String outOfBoundsMsg(int index) {
return "Index: " + index + ", Size: " + size;
}
// 节点定义
class MyNode<E> {
E data; // 数据
MyNode<E> next; //下一个节点
public MyNode(E data) {
this.data = data;
}
}
}
新增 插入
public MyNode<E> add(E e){ // 参数表示data数据 返回插入的节点
MyNode<E> newNode = new MyNode<E>(e);
// 空链表插入
if(isEmpty()){
head. = newNode;
newNode.next = head;
}else{
newNode.next = head.next;
head.next = newNode;
}
size++;
return newNode;
}
新增 在指定节点后插入
public MyNode<E> add(MyNode<E> node,E e){
MyNOde<E> newNode = new MyNode<E>(e);
if(node == null){
throw new IllegalArgumentException("Base node can not be null.");
}else{
newNode.next = node.next;
node.next = newNode;
}
size++;
return newNode;
}
删除
// 删除某结点往后的第index个结点,之后将head移动到被删结点的后一个结点
public MyNode<E> remove(MyNode<E> node,int index){
if(isEmpty()){
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}else{
MyNode<E> del; // 要删除的结点
MyNode<E> curr = node; // 当前结点
int count = 1; // 统计个数
// while(count < index + size -1){//这个内容没有看懂?
while(count < index){// 个人理解应该是这个。
count++;
cur = cur.next;
}
del = cur.next;
cur.next = del.next;
// 重置头结点
head = cur.next;
size--;
return del;
}
public MyNode<E> get(){
int index = Random.nextInt(size); // 产生一个0到指定size之间的数。
MyNode<E> cur = head;
int count = 0 ;
while(count < index){
cur = cur.next;
count ++;
}
return cur;
}
遍历
结束条件:从头结点开始 到头结点结束。
public String toString(){
StringBuilder str = new StringBuilder();
if(isEmpty()){
return "";
}
str.append(head.data.toString());
str.append(" ");
MyNode<E> cur = head.next;
while(cur != head){
str.append(cur.data.toString());
str.append(" ");
cur = cur.next;
}
retrun str.toString();
}
区别 | 栈 | 队列 |
---|---|---|
操作名称 | 进栈、出栈 | 入队、出队 |
操作限定 | 都在栈顶 | 入队在队头、出队在队尾 |
操作规则 | 后进先出 | 先进先出 |
遍历速度 | 只能从栈顶操作,需遍历整个栈才可取出,需要开辟临时空间,保证数据一致性 | 遍历速度快,基于地址指针,可以从头部或尾部进行遍历,不能同时遍历,无需开辟空间 |
应用场景 | 括号问题求解、表达式转换和求值、函数调用、递归、深度优先搜索 | 计算机系统中各种资源的管理、消息缓冲器和广度优先搜索算法 |
常用方法
方法 | 说明 |
---|---|
stack() | 创建一个空堆栈 |
boolean empty() | 测试此堆栈是否为空 |
E peek() | 查看此堆栈顶部的对象,而不从堆栈中删除它 |
E pop() | 删除此堆栈顶部的对象,并将该对象作为此函数的值返回 |
E push( E item) | 将元素推送到此堆栈的顶部 |
int search(object o) | 返回一个对象在此堆栈上的基于1的位置 |
extends Vector public int size() |
返回此向量中的组件数 |
stack如何扩容
栈构造方法只有一个,且stack继承自Vector,父类默认容量是10,它扩容的是push方法实现的;
Stack直接调用了Vector类的addElement(E item)方法;
先进先出模型
概述
数组:用于存储固定大小的同类型元素;元素是不可再分的,也可以用是数组(二维数组)。
特点
查询快、增删慢的模型
概念
散列表/哈希表(hash table),是根据关键码值(Key value)而直接进行访问的数据结构。
表示了关键码值key和记录的映射关系。
查找通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
这个映射函数叫做散列函数,存放记录的数组叫做散列表。
概念
广义表,又称列表,也是一种线性存储结构。
广义表中既可以存储不可再分的元素(原子),也可以存储广义表(子表)。
广义表存储数据常见格式
A = ():A 表示一个广义表,只不过表是空的。
B = (e):广义表 B 中只有一个原子 e。
C = (a,(b,c,d)) :广义表 C 中有两个元素,原子 a 和子表 (b,c,d)。
D = (A,B,C):广义表 D 中存有 3 个子表,分别是A、B和C。这种表示方式等同于 D = ((),(e),(b,c,d)) 。
E = (a,E):广义表 E 中有两个元素,原子 a 和它本身。这是一个递归广义表,等同于:E = (a,(a,(a,…)))。
A = () 和 A = (()) 是不一样的。前者是空表,而后者是包含一个子表的广义表,只不过这个子表是空表。
广义表 表头 表尾
当广义表不是空表时,称第一个数据(原子或子表)为"表头",剩下的数据构成的新广义表为"表尾"。
除非广义表为空表,否则广义表一定具有表头和表尾,且广义表的表尾一定是一个广义表。
案例:广义表 LS = {1} 中,表头为原子 1 ,但由于广义表中无表尾元素,因此该表的表尾是一个空表,用 {} 表示。