数据结构与算法 :
一.数据结构和算法简介
数据结构是指数据在计算机存储空间中的安排方式,而算法时值软件程序用来操作这些结构中的数据的过程.
二. 数据结构和算法的重要性
几乎所有的程序都会使用到数据结构和算法,即便是最简单的程序也不例外.比如,你希望打印出学生的名单,这个程序使用一个数组来存储学生名单,然后使用一个简单的
for循环来遍历数组,最后打印出每个学生的信息.
在这个例子中数组就是一个数据结构,而使用for循环来遍历数组,则是一个简单的算法.可见数据结构和算法是构成程序的灵魂所在,而且也有人提出数据结构+算法=程序.
简单算法
冒泡排序 :
一.核心思想 :
比较两个元素,如果前一个比后一个大则进行交换.经过对每个元素的比较,最后将最大的元素设置成最后一个元素.重复该操作,最后形成从小到大的排序.
选择排序
一.核心思想 :
扫描所有的元素,得到最小的元素,并将最小的元素与左边第一个元素进行交换.再次扫描除第一位置的所有元素,得到最小的元素,与左边第二个元素进行交换.依次类推.
例子 :
//选择排序
public void selectSort() {
int main = 0;
long tmp = 0L;
for (int i = 0; i < elems-1; i++) {
if (arr[j] < arr[min]) {
min = j;
}
}
tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
插入排序 :
一.核心思想 :
抽出一个元素,在其前面的元素中找到适当的位置进行插入.
例子 :
//插入排序
public void insertSort() {
long select = 0L;
for (int i = 11; i < elems; i++) {
select = arr[i];
int j = 0;
for (j = i; j > 0 && arr[j - 1] >= select; j--) {
arr[j] = arr[j - 1];
}
arr[j] = select;
}
}
栈 :
核心思想 : 栈只允许访问一个数据项,也就是最后插入的数据项.只有移除了找个数据项才能够访问倒数第二个插入的数据项.
例子 :
//模拟栈
package com.example.demo.testDemo;
public class MyStack {
private int maxSize;
private long[] arr;
private int top;
//构造方法
public MyStack(int size) {
maxSize = size;
arr = new long[maxSize];
top = -1;
}
// 压入数据
public void push(long value) {
arr[++top] = value;
}
// 弹出数据
public long pop() {
return arr[top--];
}
//访问栈顶元素
public long peek() {
return arr[top];
}
// 栈是否为空
public boolean isEmpty() {
return (top == -1);
}
// 栈是否满了
public boolean isFull() {
return (top == maxSize - 1);
}
}
package com.example.demo.testDemo;
public class TestStack {
public static void main(String[] args) {
MyStack ms = new MyStack(10);
ms.push(40);
ms.push(30);
ms.push(20);
ms.push(10);
ms.push(-10);
ms.push(-20);
while (!ms.isEmpty()) {
System.out.println(ms.pop());
}
}
}
队列
核心思想 : 队列是一种数据结构,类似于栈,不同的是在队列中第一个插入的数据项会最先被移除.也就是先进先出.
例子 : 循环队列
package com.example.demo.testDemo;
public class Queue {
// 数组
private long[] arr;
// 最大空间
private int maxSize;
// 有效元素大小
private int elems;
// 队头
private int font;
// 队尾
private int end;
public Queue(int maxSize) {
this.maxSize = maxSize;
arr = new long[maxSize];
elems = 0;
font = 0;
end = -1;
}
// 插入数据
public void insert(long value) {
// 循环插入
if (end == maxSize - 1) {
end = -1;
}
arr[++end] = value;
elems++;
}
// 移除数据
public long remove() {
long tmp = arr[font++];
if (font == maxSize) {
font = 0;
}
elems--;
return tmp;
}
// 是否为空
public boolean isEmpty() {
return (elems == 0);
}
// 是否满了
public boolean isFull() {
return (elems == maxSize);
}
// 返回有效元素大小
public int size() {
return elems;
}
}
优先级队列
核心思想 : 在优先级队列中,数据项按关键字的值有序,这样关键字最小的数据项(或者最大)总是在队头.数据项插入时会按照顺序插入到合适的位置以确保队列的顺序.
例子 :
package com.example.demo.testDemo;
public class PriorityQueue {
// 数组
private long[] arr;
// 最大空间
private int maxSize;
// 有效元素大小
private int elems;
public PriorityQueue(int maxSize) {
this.maxSize = maxSize;
arr = new long[maxSize];
elems = 0;
}
// 插入数据
public void insert(long value) {
int i;
for (i = 0; i < elems; i++) {
if (arr[i] < value) {
break;
}
}
for (int j = elems; j > i; j--) {
arr[j] = arr[j - 1];
}
arr[i] = value;
elems++;
}
// 移除数据
public long remove() {
long value = arr[elems - 1];
elems--;
return value;
}
// 是否为空
public boolean isEmpty() {
return (elems == 0);
}
// 是否清了
public boolean isFull() {
return (elems == maxSize);
}
//返回有效元素大小
public int size() {
return elems;
}
}
package com.example.demo.testDemo;
public class TestPQ {
public static void main(String[] args) {
PriorityQueue priorityQueue = new PriorityQueue(10);
priorityQueue.insert(30);
priorityQueue.insert(2);
priorityQueue.insert(45);
priorityQueue.insert(1);
priorityQueue.insert(15);
while (!priorityQueue.isEmpty()) {
long value = priorityQueue.remove();
System.out.println(value);
}
}
}
链接点 :
核心思想 : 链接点中包含一个数据域还一个指针域,其中数据域用来包装数据,而指针域用来指向下一个链接点.
例子 :
package com.example.demo.testDemo;
public class Link {
// 数据域
private long data;
// 指针域
private Link next;
public Link(long data) {
this.data = data;
}
public long getData() {
return data;
}
public void setData(long data) {
this.data = data;
}
public Link getNext() {
return next;
}
public void setNext(Link next) {
this.next = next;
}
}
package com.example.demo.testDemo;
public class TestLink {
public static void main(String[] args) {
Link l1 = new Link(10);
Link l2 = new Link(45);
Link l3 = new Link(360);
Link l4 = new Link(1);
l1.setNext(l2);
l2.setNext(l3);
l3.setNext(l4);
System.out.println(l1.getData());
System.out.println(l1.getNext().getData());
System.out.println(l1.getNext().getNext().getData());
System.out.println(l1.getNext().getNext().getNext().getData());
}
}
链表 :
核心思想 : 链表中只包含 一个数据项,既对第一个链接点的引用.
例子 :
package com.example.demo.testDemo;
public class LinkList {
private Link first;
public void insert(long value) {
Link link = new Link(value);
if (first == null) {
first = link;
} else {
link.setNext(first);
first = link;
}
}
public void displayAll() {
Link current = first;
while (current != null) {
System.out.println(current.getData());
current = current.getNext();
}
}
//查找指定结点
public Link find(Long key) {
Link current = first;
while (current.getData() != key) {
if (current.getNext() == null) {
return null;
}
current = current.getNext();
}
return current;
}
// 插入结点到指定位置
public void insert(long value, int pos) {
if (pos == 0) {
insert(value);
} else {
Link current = first;
for (int i = 0; i < pos - 1; i++) {
current = current.getNext();
}
Link link = new Link(value);
link.setNext(current.getNext());
current.setNext(link);
}
}
// 删除指定结点
public void delete(long key) {
Link current = first;
Link ago = first;
while (current.getData() == key) {
if (current.getNext() == null) {
return;
} else {
ago = current;
current = current.getNext();
}
}
if (current == first) {
first = first.getNext();
} else {
ago.setNext(current.getNext());
}
}
}
package com.example.demo.testDemo;
public class TestLinkList {
public static void main(String[] args) {
LinkList linkList = new LinkList();
linkList.insert(40);
linkList.insert(12);
linkList.insert(23);
linkList.insert(10);
linkList.displayAll();
System.out.println("找到结点,数据为 : " + linkList.find(10L).getData());
}
}
三角数字
核心 : 数列中的第一项为1,第n项由n-1项加n得到.
例子 :
public static int SanJiao(int n) {
int total = 0;
while(n > 0) {
total = total + n;
n--;
}
return total;
}
public static int SanJiaoDiGui(int n) {
if (n == 1) {
return 1;
} else {
return n + SanJiaoDiGui(--n);
}
}
Fibonacci数列 : 中的第1,2项为1,第n项由n-1项加n-2项得到.
public static int fibonacciMeth(int n) {
if (n == 1 || n == 2) {
return 1;
} else {
return fibonacciMeth(n-1) + fibonacciMeth(n-2);
}
}
数据结构(data structure)
是指相互之间存在一种或多种特定关系的数据元素的集合.
是组织并存储数据以便能够有效使用的一种专门格式,它用来反映一个数据的内部构成,既一个数据由那些成分数据构成,以什么方式构成,呈什么结构.
由于信息可以存在于逻辑思维领域,也可以存在于计算机世界,因此作为信息载体的数据同样存在于两个世界中.
表示一组数据元素及其相互关系的数据结构同样也有两种不同的表现形式,一种是数据结构的逻辑层面,既数据的逻辑结构;一种是存在于计算机世界的物理层面,既数据的
存储结构.
数据结构 = 逻辑结构 + 存储结构 + (在存储结构上的)运算/操作;
数据的逻辑结构
数据的逻辑结构指数据元素之间的逻辑关系(和实现无关).
分类1 : 线性结构和非线性结构
线性结构 : 有且只有一个开始结点和一个终端结点,并且所有节点都最多只有一个直接前驱和一个直接后继.
线性表就是一个典型的线程结构,它有四个基本特征 :
1.集合中必存在唯一的一个"第一个元素";
2.集合中必存在唯一的一个"最后的元素";
3.除最后元素之外,其他数据元素均有唯一的"直接后继";
4.除第一元素之外,其他数据元素均有唯一的"直接前驱".
生活案例 : 冰糖葫芦,排队上地铁
相对应线性结构,非线性结构的逻辑特征是一个结点元素可能对应多个直接前驱和多个直接后继.
常见的非线性结构有 : 树(二叉树),图(网等)
树 生活案例 : 单位组织架构,族谱.技术案例 : 文件系统.
分类2 : 线性结构,树状结构,网络结构
逻辑结构有三种基本类型 : 线性结构,树状结构和网络结构.
表和树是最常用的两种高效数据结构,许多高效的算法能够用这两个数据结构来设计实现.
线性结构 : 数据结构中线性结构值得是数据元素之间存在着"一对一"的线性关系的数据结构.
树状结构 : 除了一个数据元素(元素01)以外每个数据元素有且仅有一个直接前驱元素,但是可以有多个直接后续元素.
特点是数据元素之间是一对多的联系.
网络结构 : 每个数据元素可以有多个直接前驱元素,也可以有多个直接后续元素.特点是数据元素之间是多对多的联系.
数据的存储结构
数据的存储结构主要包括数据元素本身的存储以及数据元素之间关系表示,是数据的逻辑结构在计算机中的表示.常见的存储结构有顺序存储,链式存储,索引存储,以及散列存储.
顺序存储结构 :
把逻辑上相邻的节点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现.
由此得到的存储结构为顺序存储结构,通常顺序存储结构是借助于计算机程序设计语言(例如C/C++)的数组来描述的.
(数据元素的存储对应于一块连续的存储空间,数据元素之间的前驱和后续关系通过数据元素,在存储器中的相对位置来反映)
例如 :
0 1 ... i-1 i n-1 ...MAXSIZE-1
data a1 a2 ... ai-1 ai ai+1 ... an ...
链式存储结构 :
数据元素的存储对应的是不连续的存储空间,每个存储节点对应一个需要存储的数据元素.
每个结点是由数据域和指针域组成.元素之间的逻辑关系通过存储节点之间的链接关系反映出来.
逻辑上相邻的节点物理上不必相邻.
例如 : 数据 指针
head -> a1 -> a2 ->----> an null
头指针 表
索引存储结构 : 除建立存储结点信息外,还建立附加的索引表来标识结点的地址.
例如 : 图书,字典的目录
散列存储结构 : 根据结点的关键字直接计算出该结点的存储地址HashSet,HashMap;
一种神奇的结构,添加,查询速度快.
线性表(linear list)
线性表是n个类型相同数据元素的有限序列,通常记作(a0,a1,a2,a3,a4,...,ai,ai+1);
1.相同数据类型
在线性表的定义中,我们看到从a0到a(n-1)的n个数据元素是具有相同属性的元素.
比如说可以都是数字,例如(23,14,66,5,99);
也可以是字符,例如(A,B,C,...Z);
当然也可以是具有更复杂结构的数据元素,例如学生,商品,装备.
相同数据类型意味着在内存中存储时,每个元素会占用相同的内存空间,便于后续的查询定位.
2.序列(顺序性)
在线性表的相邻数据元素之间存在着序偶关系,
既ai-1是ai的直接前驱,则ai是ai-1的直接后续,
同时ai又是ai+1的直接前驱,ai+1是ai的直接后续.
唯一没有直接前驱的元素a0一端称为表头,
唯一没有后续的元素an-1一端称为表尾部.
除了表头和表尾元素外,任何一个元素都有且仅有一个直接前驱和直接后继.
3.有限
线性表中数据元素的个数n定义为线性表的长度,n是一个有限值.
当n=0时线性表为空表.
在非空的线性表中每个数据元素在线性表中都有唯一确定的序号,例如a0的序号是0,ai的序号是i.
在一个具有n > 0个数据元素的线性表中,数据元素序号的范围是[0,n-1].
生活案例 : 冰糖葫芦,多个学生分数,多个学生数据.
顺序表 —顺序存储结构
0 1 ... i-1 i n-1 ...MAXSIZE-1
data a1 a2 ... ai-1 ai ai+1 ... an ...
last
特点 : 在内存中分配连续的空间,只存储数据,不需要存储地址信息.位置就隐含着地址.
优点 :
1.节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑C/C++语言中数组需指定大小的情况),结点之间逻辑关系没有占用额外的存储空间.
2.索引查找效率高,既每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址.
假设线性表的每个数据元素需占用K个存储单元,并以元素所占的第一个存储单元的地址作为数据元素的存储地址.
则线性表中序号为i的数据元素的存储地址LOC(ai)与序号为i+1的数据元素的存储地址LOC(ai+1)之间的关系为
LOC(ai+1) = LOC(ai) + K
通常来说,线性表的i号元素ai的存储地址为
LOC(ai) = LOC(a0) + i*K
其中LOC(a0)为0号元素a0的存储地址,通常称为线性表的起始地址.
缺点 :
1.插入和删除操作需要移动元素,效率较低.
2.必须提前分配固定数量的空间,如果存储元素少,可能导致空闲浪费.
3.按照内容查询效率低,因为需要逐个比较判断.
链表 —链式存储结构
数据 指针
head -> a1 -> a2 ->----> an null
头指针 表
特点 : 数据元素的存储对应的是不连续的存储空间,每个存储结点对应一个需要存储的数据元素.
每个结点是由数据域和指针域组成.元素之间的逻辑关系通过存储节点之间的链接关系反映出来.
逻辑上相邻的节点物理上不必相邻.
缺点 :
1.比顺序存储结构的存储密度小(每个节点都由数据域和指针域组成,所以相同空间内假设全存满的话顺序比链式存储更多).
2.查找结点时链式存储要比顺序存储慢(每个节点地址不连续,无规律,导致按照索引查询效率低下).
优点 :
1.插入,删除灵活(不必移动节点,只要改变节点中的指针,但是需要先定位到元素上).
2.有元素才会分配结点空间,不会有闲置的结点.
在使用单链表实现线性表的时候,为了使程序更加简洁,我们通常在单链表的最前面添加一个哑元结点,也称为头结点.
在头结点中不存储任何实质的数据对象,其next域指向线性表中0号元素所在的结点,可以对空表,非空表的情况以及首元结点进行统一处理,编程更方便,常用头结点.
一个带头结点的单链表实现线性表的结构图如图所示.
双向链表
单链表一个优点是结构简单,但是它也有一个缺点,既在单链表中只能通过一个结点的引用访问其后续结点,而无法直接访问其前驱结点,要在单链表中找到某个结点的前驱
结点,必须从链表的首结点出发依次向后寻找,但是需要O(n)时间.
为此我们可以扩展单链表的结点结构,使得通过一个结点的引用,不但能够 访问其后续结点,也可以方便的访问其前驱结点.扩展单链表结点结构的方法是,在单链表结点
结构中新增加一个域,该域用于指向结点的直接前驱结点.扩展后的结点结构是构成双向链表的结点结构,如图所示.
前驱指针域 数据域 后续指针域
pre data next
双向链表是通过上述定义的结点使用pre以及next域依次串联在一起而形成的.一个双向链表的结构如图所示.
head tail
^ a0 -> a1 -> a2 -> a3^
<- <- <-
在双向链表中同样需要完成数据元素的查找,插入,删除等操作.在双向链表中进行查找与在单链表中类似,只不过在双向链表中查找操作可以从链表的首结点开始,也可以
从尾结点开始,但是需要的时间和在单链表中一样.
循环链表
在一个循环链表中,首节点和末节点被连接在一起.这种方式在单向和双向链表中皆可实现.要转换一个循环链表,你开始于任意一个节点然后沿着列表的任一方向直到返回
开始的节点.循环链表可以被视为"无头无尾".
循环链表中第一个节点之前就是最后一个节点,反之亦然.循环链表的无边界使得在这样的链表上设计算法会比普通链表更加容易.对于新加入的节点应该是在第一个节点
之前还是最后一个节点 之后可以根据实际要求灵活处理,区别不大.
单向列表的循环带头结点的非空链表.
| - - - - - - - -- - -|
->^ ->a1 -> a2 ->... ->an -|
单向链表的循环带头结点的空链表
双向链表的循环带头结点的非空链表
栈和队列都是操作受限的线性表.
栈的定义
栈(stack)又称堆栈,它是运算受限的线性表.
其限制是仅允许在表的一端进行插入和删除操作,不允许在其他任何位置进行插入,查找,删除等操作.
表中进行插入,删除操作的一端称为栈顶(top),栈顶保存的元素称为栈顶元素.
相对的,表的另一端称为栈底(bottom)
当栈中没有数据元素时称为空栈;向一个栈插入元素又称为进栈或入栈;从一个栈中删除元素又称为出栈或退栈.由于栈的插入和删除操作仅在栈顶进行,后进栈的元素必定
先出栈,所以又把堆栈称为后进先出表(LIFO) <- 栈顶
D <-栈顶
C C
<-栈顶 B B
<-栈顶/底 A <-栈底 A <- 栈底 A <-栈底
空栈 A入栈 BCD入栈 D出栈
生活案例 : 摞盘子和取盘子,一摞书,酒被塔(各层之间可以简单理解为栈,每层内部不是栈)
技术案例 : Java的栈内存.
栈可以保存底层方法的信息.
队列定义
队列(queue)简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除.在队列中把插入数据元素的一端称为队尾
(rear),删除数据元素的一端称为队首(front).向对尾插入元素称为进队或入队,新元素入队后成新的队尾元素;从队列中删除元素称为离队或出队,元素出队后,其后续元素
成为新的队首元素.由于队列的插入和删除操作分别在队尾和队首进行,每个元素必然按照进入的次序离队,也就是说先进队的元素必然先离队,所以称队列为先进先出表(FIFO)
双端队列deque 通常为deck
所谓双端队列是指两端都可以进行进队和出队操作的队列, 如下图所示,将队列的两端分别称为前端和后端,两端都可以入队和出队.其元素的逻辑结构任是线性结构.
前端进 后端进
前端 <--> 后端
前端出 <--> 后端出
在双端队列进队时 : 前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面.在双端队列出队时,无论前端出还是后端出,先出
的元素排列在后出的元素的前面.
输出受限的双端队列,既一个端点运行插入和删除,另一个端点只允许插入的双端队列.
前端进
<-> 前端 后端 <- 后端进
前端出
输入受限的双端队列,既一个端点运行插入和删除,另一个端点只允许删除的双端队列.
前端出 后端进
前端 <- 后端 <->
后端出
双端队列既可以用来队列操作,也可以用来实现栈操作(只操作一端就是栈了)
树
树是由一个集合以及在该集合上定义的一种关系构成的.集合中的元素称为树的结点,所定义的关系称为父子关系.
父子关系在树的结点之间建立了一个层次结构.树的结点包含一个数据元素及若干指向其子树的若干分之.在这种层次结构中有一个结点具有特殊的地位,这个结点称为该
树的根结点,或简称为树根.
我们可以形式地给出树的递归定义如下 :
树(tree)是n(n>=0)个结点的有限集.它
1>或者是一颗空树(n=0),空树不包含任何结点.
2>或者是一颗非空树(n>0),此时有且仅有一个特定的称为根(root)的结点;
当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集T1,T2,,,,,,Tm.其中每一个本身又是一棵树,并且称为根的子树(sub tree).
(a) A
A / | \
(b) B C D
/ \ | /|\
E F G H I J
/\ |
K L M
例如图 (a)是一颗空树,(b)是只有一个根节点的树,(c)是一颗有10个节点的树,其中A是根,其余的节点分成3个不相交的集合 : T1 = {B,E,F}, T2 = {C,G}, T3 = {D,H,J},
每个集合都构成一棵树,且都是根A的子树.
生活案例 : 树 : 单位组织架构,族谱.
结点的度与树的度
结点拥有的子树的数目称为结点的度(Degree).
度为0的结点称为叶子(leaf)或终端节点.度不为0的结点称为非终端结点或分支结点.除根之外的分支结点也称为内部结点.树内各结点的度最大值称为树的度.
上图中A : 此结点度为3; K : 此结点度为0; H : 此结点度为1; 此树的度为3
父亲,儿子,兄弟
父亲(parent) : 一个结点的直接前驱结点.
儿子(child) : 一个结点的直接后继结点.
兄弟(sibling) : 同一个父亲结点的其他结点.
结点A是结点B,C,D的父亲,结点B,C,D是结点的A的孩子.
由于结点H,I,J有同一个父结点D,因此他们互为兄弟.
祖先,子孙,堂兄弟
将父子关系进行扩展,就可以得到祖先,子孙,堂兄弟等关系.
结点的祖先是从根到该结点路径上的所有结点.
以某结点为根的树中的任一结点都称为该结点的子孙.
父亲在同一层次的结点互为堂兄弟.
二叉树 :
每个结点的度均不超过2的有序树,称为二叉树(binary tree).(树中每个节点最多只能有两个子节点)
与树的递归定义类似,二叉树的递归定义如下 :
二叉树或者是一颗空树,或者是一颗由一个根结点和两颗互不相交的分别称为根的左子树和右子树的子树所组成的非空树.
例如 :
E
/\
A G
\ \
C F
/\
B D
图1
由以上定义可以看出 :
二叉树中每个结点的孩子数只能是0,1或2个,并且每个孩子都有左右之分.位于左边的孩子称为左孩子,位于右边的孩子称为右孩子;以左孩子为根的子树称为左子树,
以右孩子为根的子树称为右子树.
插入节点
核心思想 :
1.如果不存在节点,则直接插入.
2.从根开始查找一个相应的节点,既新节点的父节点,当父节点找到后,根据新节点的值来确定新节点是连接到左子节点还是右子节点.
查找节点
核心思想 :
1.从根开始查找,如果查找节点值比父节点值要小,则查找其左子树,否则查找其右子树,直到查到为止,如果不存在节点,则返回null.
修改节点
核心思想 :
1.首先查找节点,找到相应节点后,再进行修改.
遍历二叉树
核心思想 : 分为三种方法,一种是先序遍历,一种是中序遍历,一种是后序遍历.
先序遍历二叉树
核心思想 : 访问节点.遍历节点的左子树,遍历节点的右子树.
中序遍历二叉树
核心思想 : 中序遍历节点的左子树,访问节点,中序遍历节点的右子树.
例子 :
package com.example.demo.TreeTest;
public class Node {
// 关键字
private int keyData;
// 其他数据
private int otherData;
// 左子结点
private Node leftNode;
// 右子节点
private Node rightNode;
public Node(int keyData, int otherData) {
this.keyData = keyData;
this.otherData = otherData;
}
public int getKeyData() {
return keyData;
}
public void setKeyData(int keyData) {
this.keyData = keyData;
}
public int getOtherData() {
return otherData;
}
public void setOtherData(int otherData) {
this.otherData = otherData;
}
public Node getLeftNode() {
return leftNode;
}
public void setLeftNode(Node leftNode) {
this.leftNode = leftNode;
}
public Node getRightNode() {
return rightNode;
}
public void setRightNode(Node rightNode) {
this.rightNode = rightNode;
}
// 显示方法
public void disPlay() {
System.out.println(keyData + "," + otherData);
}
}
package com.example.demo.TreeTest;
public class Tree {
// 根
private Node root;
// 插入方法
public void insert(int keyData, int otherData) {
Node node = new Node(keyData, otherData);
// 如果说没有节点
if (root == null) {
root = node;
} else {
Node current = root;
Node parent;
while (true) {
parent = current;
if (keyData < current.getKeyData()) {
current = current.getLeftNode();
if (current == null) {
parent.setLeftNode(node);
return;
}
} else {
current = current.getRightNode();
if (current == null) {
parent.setRightNode(node);
return;
}
}
}
}
}
// 查找方法
public Node find(int keyData) {
Node current = root;
while (current.getKeyData() != keyData) {
if (keyData < current.getKeyData()) {
current = current.getLeftNode();
} else {
current = current.getRightNode();
}
if (current == null) {
return null;
}
}
return current;
}
// 删除方法
public void delete(int keyData) {
}
// 修改方法
public void change(int keyData, int newOtherData) {
Node node = find(keyData);
node.setOtherData(newOtherData);
}
// 先序遍历方法
public void preOrder(Node node) {
if (node != null) {
node.disPlay();
preOrder(node.getLeftNode());
preOrder(node.getRightNode());
}
}
}
package com.example.demo.TreeTest;
public class TestTree {
public static void main(String[] args) {
Tree tree = new Tree();
tree.insert(1, 1);
tree.insert(2, 2);
tree.insert(3, 3);
Node findnODE = tree.find(3);
findnODE.disPlay();
}
}
满二叉树 :
高度为k并且有2(k+1)-1个结点的二叉树.
在满二叉树中,每层结点都达到最大数,既每层结点都是满的,因此称为满二叉树.
完全二叉树 :
若在一颗满二叉树中,在最下层从最右侧起去掉相邻的若干叶子结点,得到的二叉树即为完全二叉树.
1 1
/\ /\
2 3 2 3
/\ /\ /\ /\
4 5 6 7 4 5 6 7
/\ /\ /\ /\ /\ /\ /
8 9 10 11 12 13 14 15 8 9 10 11 12
(a)满二叉树 (b)完全二叉树
满二叉树必为完全二叉树,而安全二叉树不一定是满二叉树.
图1
二叉树的存储结构 :
二叉树的存储结构有两种 : 顺序存储结构和链式存储结构.
链式存储结构
设计不同的结点结构可构成不同的链式存储结构.
在二叉树中每个结点都有两个孩子,则可以设计每个结点至少包括3个域 : 数据域,左孩子和右孩子域.
数据域存放数据元素,左孩子域存放指向左孩子结点的指针,右孩子域存放指向右孩子结点的指针.如图(a)所示.
利用此结点结构得到的二叉树存储结构称为二叉链表.
为了方便找到父结点,可以在上述结点结构中增加一个指针域,指向结点的父结点.如图(b)所示.
采用此结点结构得到的二叉树存储结构称为三叉链表.
1
/\
4 2
\ /\
5 3 6
\
7
遍历(Traverse) :
就是按照某种次序访问树中的所有结点,且每个结点恰好访问一次.也就是说,按照被访问的次序,可以得到由树中所有结点排成的一个序列.树的遍历也可以看成是人为的将
非线性结构线性化.这里的"访问"是广义的,可以 是对结点作各种处理,例如输出结点信息,更新结点信息等.在我们的实现中,并不真正的"访问"这些结点,而是得到一个结点
的线性序列,以线性表的形式输出.
将整个二叉树看做三部分 : 根,左子树,右子树,如果规定先遍历左子树,再遍历右子树.
那么根据根的遍历顺序就有三种遍历方式 :
先序/根遍历DLR : 根,左子树,右子树
中序/根遍历LDR : 左子树,根,右子树
后根/序遍历LRD : 左子树,右子树,根
注意 : 由于树的递归定义,其实对三种遍历的概念其实也是一个递归的描述过程.
先序遍历DLR : 1 4 5 2 3 6 7
中序遍历LDR : 4 5 1 3 2 6 7
后序遍历LRD : 5 4 3 7 6 2 1
面试题 : 已知一颗二叉树的后序遍历的序列为5 4 3 7 6 2 1, 中序遍历的序列为 4 5 1 3 2 6 7,则其先序遍历的序列是什么?
(首先明白一点,只给先序和后序无法求出中序的,中序是必须给出的)
先序遍历为 : 1,4,5,2,3,6,7
1
/
4 2
\ /
5 3 6
7
例如 :
E
/
A G
\
C F
/
B D
先序遍历(根,左子树,右子树) 中序遍历(左子树,根,右子树) 后序遍历(左子树,右子树,根) 层次遍历(借助队列来实现)
E,A,C,B,D,G,F A,B,C,D,E,G,F B,D,C,A,F,G,E E,A,G,C,F,B,D
二叉查找/搜索/排序树 BST(binary search/sort tree)
或者是一颗空树 :
或者是具有下列性质的二叉树 :
(1) 若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值;
(2) 若它的右子树上所有结点的值均大于它的根节点的值;
(3) 它的左,右子树叶分别为二叉树.例子 :
7 8
/\ /\
3 17 3 10
/\ /\ /\ \
2 4 13 20 1 6 14
/\ / /\ /
9 15 18 4 7 13
3 A
/\ \
2 8 B
/\ / \
2 3 6 C
/ \
5 D
/ \
4 E
注意 : 对二叉查找树进行中序遍历,得到有序集合.
平衡二叉树(Self-balancing binary search tree) 自平衡二叉查找树,又被称为AVL树 (有别于AVL算法)
它是一颗空树或它的左右两个子树的高度差(平衡因子)的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树,同时,平衡二叉树必定是二叉搜索树,反之则不一定
平衡因子(平衡度) : 结点的平衡因子是 结点的左子树的高度减去右子树的高度.(或反之定义)
平衡二叉树 : 每个结点的平衡因子都为1,-1,0的二叉排序树.或者说每个结点的左右子树的高度最多差 1的二叉排序树.
平衡二叉树的目的是为了减少二叉查找树层次,提供查找速度.
平衡二叉树的常用实现方法有AVL,红黑树,替罪羊树 ,Treap,伸展树等.
例子 :
20
/\
10 30
/\ /\
7 14 25 40
/ \
4 8
平衡二叉树
红黑树
R-B Tree,全称是Red-Black Tree,又称为"红黑树",它是一种平衡二叉树.红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black).
红黑树的特性 :
(1) 每个节点或者是黑色,或者是红色.
(2) 根节点时黑色.
(3) 每个叶子节点(NIL)是黑色.[注意 : 这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的.
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点.
注意 :
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点.
(02) 特性(5),确保没有一条路径会比其他路径长出两倍.因而,红黑树是相对是接近平衡的二叉树.
例如 :
80
/\
40 120
/\ /\
20 60 100 140
/\ /\ /\ /\
10 NIL 50 NIL 90 NIL 10 30
/\ /\ /\ /\
NIL NIL NIL NIL NIL NIL NIL NIL
红黑树
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(logN),效率非常之高.
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的 : 它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目.
例如 : java集合中的TreeSet和TreeMap,C++STL中的set,map,以及linux虚拟内存的管理,都是通过红黑树去实现的.
图的基本概念 : 多对多关系
图(graph)是一种网站数据结构,图是由非空的顶点集合和一个 描述顶点之间关系的集合组成.
其形式化的定义如下 :
Graph = (V,E)
V = {x|x<-某个数据对象}
E = {<u,v>|P(u,v)^(u,v<-V)}
V是具有相同特性的数据元素的集合,V中的数据元素通常称为顶点(Vertex),
E是两个顶点之间关系的集合.P(u,v)表示u和v之间有特定的关联属性.
若<u,v><-E,则<u,v>表示从顶点v的一条弧,并称u为弧尾或起始点,称v为弧头或终止点,此时图中的顶点之间的连线是有方向的,这样的图称为有向图(directedgraph).
若<u,v><-E,则必有<u,v><-E,既关系E是对称的,此时可以使用一个无序对(u,v)来代替两个有序对象,它表示顶点u和顶点v之间的一条边,此时图中顶点之间的连线是没有
方向的,这种图称为无向图(undirected graph).
在无向图和 有向图中V中的元素都称为顶点,而顶点之间的关系却有不同的称谓,既弧或边,为避免麻烦,在不影响理解的前提下,我们统一的将它们称为边(edge).
并且我们还约定顶点集与边集都是有限的,并记顶点与边的数量为|V|和|E|.
a -- d a --> d
|\ | /|\ |
| c | | \|/
b \c b c
无向图实际上也是有向图,是双向图.
加权图 :
在实际应用中,图不但需要表示元素之间是否存在某种关系,而且图的边往往与具有一定实际意义的数有关,既每条边都有与它相关的实数,称为权.
这些权值可以表示从一个订单到另一个顶点的距离或消耗等信息,在本章中假设边的权均为正数.这种边上具有权值的图称为带权图(weighted graph)
图的遍历 :
图的遍历是和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使得每一个顶点仅被访问一次,这一过程就叫做图的遍历.
深度优先遍历 :
也有称为深度优先搜索,简称为DFS.
它从图中某个顶点V出发,访问此顶点,然后从V的未被访问的邻接点出发深度优先遍历图,直至图中所有和V有路径相遇的顶点都被访问到.
例子 :
publi class Graph {
private int vertexSize; //顶点数量
private int[] vertexs; //顶点数组
private int[][] matrix;
private static final int MAX_WEIGHT = 1000;
private boolean[]isVisited;
public Graph(int vertextSize) {
this.vertexSize = vertextSize;
matrix = new int[vertextSize][vertextSize];
vertexs = new int[vertextSize];
for (int i = 0; i < vertextSize; i++) {
vertexs[i] = i;
}
isVisited = new boolean[vertexSize];
}
/**
获取某个顶点的第一个邻接点
*/
public int getFirsNeighbor(int index) {
for (int j = 0; i < vertexSize; j++) {
if (matrix[index][j] > 0 && matrix[index][j] < MAX_WEIGHT) {
return j;
}
}
return -1;
}
/**
根据前一个邻接点的下标来取得下一个邻接点
v1 : 表示要照的顶点
v2 : 表示该顶点相对于哪个邻接点去获取下一个邻接点
*/
public int getNextNeighbor(int v, int index) {
for (int j = index + 1; j < vertexSize; j++) {
if (matrix[v][j] > 0 && matrix[v][j] < MAX_WEIGHT) {
return j;
}
}
return -1;
}
/**
图的深度优先遍历算法
*/
public void depthFirstSearch(int i) {
isVisited[i] = true;
int w = getFirstNeighbor(i);
while (w != -1) {
if (!isVisited[w]) {
// 需要遍历该顶点
System.out.println("访问到了 : " + w + "顶点");
depthFirstSearch(w);
}
// 第一个相对于w的邻接点
w = getNextNeighbor(i, w);
}
}
/**
对外公开的深度优先遍历
*/
public void depthFirstSearch() {
isVisited = new Boolean[vertextSize];
for (int i = 0; i < vertexSize; i++) {
if (isVisited[i]) {
depthFirstSearch(i);
}
}
isVisited = new Boolean[vertexSize];
}
public void broadFirstSearch() {
isVisited = new boolean[vertexSize];
for (int i = 0; i < vertexSize; i++) {
if (!isVisited[i]) {
broadFirstSearch(i);
}
}
}
/**
实现广度优先遍历
*/
public void broadFirstSearch(int i) {
int u,w;
LinkedList<Integer> queue = new LinkedList<Integer>();
System.out.println("访问到 : " + i + "顶点");
isVisited[i] = true;
queue.add(i); // 第一次把V0加到队列
while (!queue.isEmpty()) {
u = (Integer)(queue.removeFirst()).intValue();
w = getFirstNeighbor(u);
while (w != -1) {
if (!isVisited[w]) {
System.out.println("访问到了 : " + w + "顶点");
isVisited[w] = true;
queue.add(w);
}
w = getNextNeighbor(u, w);
}
}
}
}
最小生成树
假设你是电信的实施工程师,需要为一个镇的九个村庄架设通信网络做设计,村庄位置大致如图7-6-1,其中v0~v8是村庄,之间连线的数字表示村与村间的可通达的直线距离
进行设计?
一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边.我们把构造连通网的最小代价生成树.称为最小生成树.
找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法
例子 :
/**
prim 普里姆算法(先构造邻接矩阵)
*/
public void prim() {
// 最小代价顶点权值的数组,为0表示已经顶点(自己)
int[] lowcost = new int[vertexSize];
// 放顶点权值
int[] adjvex = new int[vertexSize];
for (int i = 1; i < vertexSize; i++) {
lowcost[i] = matrix[0][i];
}
for (int i = 1; i < vertexSize; j++) {
min = MAX_WEIGHT;
minId = 0;
for (int j = 1; j < vertexSize; j++) {
if (lowcost[j] < min && lowcost[j] > 0) {
min = lowcost[j];
minId = j;
}
}
System.out.println("顶点 : " + adjvex[minId] + "权值 : " + min);
sum += min;
lowcost[minId] = 0;
for (int j = 1; j < vertexSize; j++) {
if (lowcost[j] != 0 && matrix[minId][j] < lowcost[j]) {
lowcost[j] = matrix[minId][j];
adjvex[j] = minId;
}
}
}
System.out.println("最小生成树权值和 : " + sum);
}
克鲁斯卡尔算法实现 :
例子 :
package com.example.demo.graph;
public class GraphKruskal {
private Edge[] edges;
private int edgeSize;
private GraphKruskal(int edgeSize) {
this.edgeSize = edgeSize;
edges = new Edge[edgeSize];
}
public void miniSpanTreeKruskal() {
int m,n,sum = 0;
// 神奇的数组,下标为起点,值为终点
int[] parent = new int[edgeSize];
for (int i = 0; i < edgeSize; i++) {
parent[i] = 0;
}
for (int i = 0; i < edgeSize; i++) {
n = find(parent, edges[i].begin);
m = find(parent, edges[i].end);
if (n != m) {
parent[n] = m;
System.out.println("起始顶点 : " + edges[i].begin + "---结束顶点 : " + edges[i].end + "~权值 : " + edges[i].weight);
sum += edges[i].weight;
} else {
System.out.println("第 " + i + "条边回环了");
}
}
System.out.println("sum : " + sum);
}
/**
* 将神奇数组进行查询获取非回环的值
*/
public int find(int[] parent, int f) {
while(parent[f] > 0) {
System.out.println("找到起点 : " + f);
f = parent[f];
System.out.println("找到终点 : " + f);
}
return f;
}
public void createEdgeArray() {
Edge edge = new Edge(4, 7, 7);
Edge edge1 = new Edge(2, 8, 8);
Edge edge2 = new Edge(0, 1, 10);
Edge edge3 = new Edge(0, 5, 11);
Edge edge4 = new Edge(1, 8, 12);
Edge edge5 = new Edge(3, 7, 16);
Edge edge6 = new Edge(2, 8, 8);
}
class Edge {
private int begin;
private int end;
private int weight;
public Edge(int begin, int end, int weight) {
super();
this.begin = begin;
this.end = end;
this.weight = weight;
}
public int getBegin() {
return begin;
}
public void setBegin(int begin) {
this.begin = begin;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
}