目录
数据结构
数据结构是计算机存储、组织数据的方式。它表示的是数据的逻辑结构、数据的物理结构以及两者之间的相互关系,一个好的数据结构可以带来更高的运行或者存储效率。
在计算机中,存储数据的方式可以概括为两种
-
开辟一块存储空间,把每条数据挨个连续的存储在里面
-
每一条数据中添加一些内容表示下一条数据所在位置,这样数据就可以在存储空间中散乱存储
这是两种最基本的数据结构,数组和链表的原型,其它复杂的数据结构无非也就是这两种数据结构的组合和扩展
一、数组
数组就是在内存中开辟一块固定大小的内存空间,我们把每条数据挨个的存进去,数组的引用(数组名)指向这块内存的首地址,从首地址开始存储。因为每种数据类型的占用空间的大小不一样,为了方便存取元素和提高内存的利用率,数组中只能存储同一种数据类型的数据。
因为数组存的是同一种数据类型,所以数组是把内存空间划分成若干个相同大小的区域,每一块存储一条数据,后一条数据的地址就是前一条数据的地址加上存储一条数据的大小(数据类型所占的空间),这样,我们就能很快的访问到数组中的任一条数据,但是如果我们想要在数组中间添加或者删除数据的话,就需要对后面的元素进行移动,这非常麻烦
-
数组的存储特点
-
数组存储的内容在内存空间中是连续的
-
只能存储同一种数据类型的数据
-
-
数组的特点
-
访问快
-
增删慢
-
二、链表
-
链表的存储结构
百度百科的介绍
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表就是由一个一个的结点组成的,每一个结点中保存有这一个结点的内容和下一个结点的地址,通过内存地址就可以找到下一个结点
这样我们就不需要像数组一样提前规定好大小,可以自由的控制结点的个数,我们用链表来存储元素就变的非常的灵活,增删元素只需要改变结点中存的地址即可。
但这样也带来了新的问题,当我们想要获取一个结点时,就必须得先得到它的上一个结点,从而通过地址来找到它,因此链表的查询速度要比数组要慢
-
链表的特点
-
增删元素块
-
查询元素慢
-
-
链表的简单实现
public class LinkedList {
//一个表示结点的静态内部类
private static class Node{
Object node;//存储本结点的内容
Node next;//指向下一个结点
//构造方法
public Node(Object node, Node next) {
this.node = node;
this.next = next;
}
//tostring方法,用于测试是打印数据
@Override
public String toString() {
return "Node{" +
"node=" + node +
'}';
}
}
//头结点
private Node header;
//记录元素个数
private int size = 0;
//简单的向链表中添加元素的方法
public void add(Object o){
Node e = new Node(o, null);
if (header == null) {
header = e;
size++;
return;
}
e.next = header;
header = e;
size++;
}
//返回链表的大小
public int getSize(){
return size;
}
}
三、栈
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一端称为栈顶(top),另一端为栈底(bottom);栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈(PUSH),删除则称为退栈(POP)。栈也称为先进后出表。
栈可以用来在函数调用的时候存储断点,做递归时要用到栈
栈在程序的运行中有着举足轻重的作用。最重要的是栈保存了一个函数调用时所需要的维护信息,这常常称之为堆栈帧或者活动记录
-
栈的存储结构
-
栈的特点
-
栈是一种特殊的线性表
-
只允许在一端对元素进行操作
-
元素先进后出
-
-
栈的简单实现
-
使用数组实现
-
public class Stack<T> {
//存储元素的数组
private Object[] elements;
//默认容量容量
private int DEFAUT_CAP = 1 << 4;
//记录栈中元素个数
private int size;
//无参构造
public Stack(){
size = 0;
//初始化数组
elements = new Object[DEFAUT_CAP];
}
//返回元素个数
public int size(){
return size;
}
//存放元素
public void push(T t){
if (size >= DEFAUT_CAP){
throw new StackOverflowError("栈容量不足");
}
elements[size] = t;
size++;
}
//取出元素,只能从栈顶取
public T get(){
T t = null;
if (size == 0)
return null;
t = (T)elements[size - 1];
elements[size - 1] = null;
size--;
return t;
}
}
-
-
使用链表实现
-
public class StackByLinkedList<T> {
//header指向首元素
private Node<T> header;
//size记录元素个数
private int size;
//构造方法
public StackByLinkedList() {
size = 0;
}
//返回元素个数
public int size(){
return size;
}
//存元素
public void push(T t){
Node<T> node = new Node<>(t, null);
if (header == null)
header = node;
else{
node.next = header;
header = node;
}
size++;
}
//取元素
public T get(){
if (header == null)
return null;
T t = header.elem;
header = header.next;
size--;
return t;
}
//Node类表示每一个结点
private static class Node<T>{
T elem;
Node next;
public Node(T elem, Node next) {
this.elem = elem;
this.next = next;
}
}
}
四、队列
队列和栈相似,不同的是队列只允许在一端进行插入数据,在另一端进行取出数据,一般来说,进行插入操作的一端称为队尾,进行删除操作的一端称为队头。队列中没有元素时,称为空队列。
-
队列的存储结构
-
队列的特点
-
队列是一种线性表
-
队列只允许在一端插入数据,在另一端取出数据
-
-
队列的实现分析
当用数组来实现队列时,需要两个指针,一个是指向头元素的head,一个是指向尾元素tail
假设数组长度为5,开始时,队列为空,即head = tail = 0;
现在往队列中存4个元素,此时head = 1,tail = 4,
取出队列的两个元素,head = 3,tail = 4;
再往队列中添加一个元素,head = 3,tail = 5,这时tail已经到达数组的末尾了,已经无法在往队列中存元素,但其实数组中头两个位置还是空的,这就造成了假溢出现象
怎么解决假溢出现象
-
可以在取出元素后,数组中的元素都向前移动,显然这种方法是很浪费时间的
-
可以使用链表替代数组,每次插入取出都需要动态的创建和销毁结点,效率太低
-
可以使用循环队列的方法
将数组存储区看成是一个首尾相接的环形区域,当存放到n地址后,下一个地址就"翻转"为1
-
-
循环队列的实现
循环队列有如下特点
-
空队列时,head = tail = 0;
-
存元素时,tail = tail + 1;
-
若tail = n + 1时,tail = 1;
-
-
tail = head时,队列存满,但tail = head也可能是队列为空,为了区别这种情况,我们规定,当队列里还有一个空位时就存满了
-
public class Queue<T> {
//存放元素的数组
private static Object[] elementData;
//默认数组长度
private static int DEFAULT_CAP = 1 << 4;
//头指针,指向队首元素
private int head;
//尾指针,指向队尾元素的下一个位置
private int tail;
//构造方法
public Queue(){
elementData = new Object[DEFAULT_CAP];
head = tail = 0;
}
//判断数组是否为空
public boolean isNull(){
return tail == head;
}
//判断数组是否存满,规定数组中头尾指针间还有一个空位子时为满
public boolean isFull(){
if ((tail + 1)%elementData.length == head)
return true;
return false;
}
//存元素
public void add(T t){
if (isFull())
throw new IndexOutOfBoundsException("队列已经满了");
elementData[tail] = t;
tail = (++tail) % elementData.length;
}
//取元素
public T get(){
if (isNull())
throw new IndexOutOfBoundsException("队列为空");
return (T)elementData[(head = (head+1)%elementData.length) - 1];
}
//获取元素个数
public int size(){
return tail - head;
}
}
五、树
树是一种数据结构,它是由n(n≥1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点 可以分为多个不相交的子树。
百度百科
简单的说,树可以理解成一种特殊的链表,上一个节点可以同时指向多个下一个节点,但一个节点最多只能指向一个上节点,这有点类似Java中的继承关系。没有上一个节点的节点是树的开始,即树根,没有下一个节点的节点叫做树叶
-
树的存储结构
-
树的相关概念
-
深度:即树有多少层,只有根节点的树深度为1
-
节点的度:一个节点含有的子节点的个数称为该节点的度,叶节点的度为0
-
-
树的遍历
-
先序遍历:按根-->左子节点-->右子节点的顺序进行遍历(根左右)
-
中序遍历:按左子节点-->根-->右子节点的顺序进行遍历(左根右)
-
后序遍历:按左子节点-->右子节点-->根的顺序进行遍历(左右根)
-
-
树的实现
下面实现了一个任意树,以下代码为个人理解所写,仅供参考
public class Tree<T> {
//树的根节点
private Node root;
//树中元素的个数
private int size;
//树的节点结构
private class Node{
Node parent;
T item;
ArrayList<Node> child;
public Node(T item, Node parent) {
this.item = item;
this.parent = parent;
this.child = new ArrayList<>();//这里可以用数组代替
}
public Node(T item) {
this.item = item;
this.child = new ArrayList<>();
}
public Node() {
this.child = new ArrayList<>();
}
}
//构造方法
public Tree() {
this.root = null;
this.size = 0;
}
//添加元素
//默认添加到根节点
public void add(T t){
if (this.root == null){
root = new Node(t);
size++;
}
else
add(t,root.item);
}
//指定父节点添加
public void add(T t,T parent){
Node node = get(root,parent);
Node n = new Node(t,node);
node.child.add(n);
size++;
}
//查找指定元素的节点
private Node get(Node n,T t){
Node nn = null;
if (n == null)
return null;
if (n.item == t)
return n;
else{
for (Node node : n.child) {
nn = get(node,t);
if (nn != null)
return nn;
}
}
return nn;
}
//删除指定元素
//删除后它的子元素指向它的父元素
public void delete(T t){
Node node = get(root,t);
if (node == null)
throw new ElementNotFond("要删除的元素不存在!");
for (Node n : node.child) {
n.parent = node.parent;
node.parent.child.add(n);
}
size--;
}
//修改元素内容
public void update(T oldValue,T newValue){
Node node = get(root, oldValue);
node.item = newValue;
}
//获取数的元素个数
public int size(){
return size;
}
}
六、二叉树
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树
百度百科
二叉树就是每个节点最多只能有两个子节点的树
-
存储结构
-
-
空二叉树:没有元素的二叉树
-
只有一个根节点的二叉树
-
只有左子树
-
只有右子树
-
完全二叉树:左右子元素都存在
-
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树
-
完全二叉树:从根节点到倒数第二层是一个满二叉树,最后一层可不填满,但都是靠左的
-
-
二叉树的实现
public class BinaryTree<T> {
//二叉树的结点结构
private static class Node<T>{
T item;
Node leftChild;
Node rightChild;
public Node(T item, Node leftChild, Node rightChild) {
this.item = item;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
public Node(T item) {
this.item = item;
}
public Node() {
}
}
//根节点元素
private Node<T> root;
//记录二叉树中元素个数
private int size;
//返回元素个数
private int i = 0;
public int size(){
return size;
}
//返回根节点
public T getRoot(){
return root.item;
}
//添加元素,左孩子结点<根节点<右孩子结点
public void add(T t){
if (root == null) {
root = new Node(t);
size++;
return;
}
add(t,root);
size++;
}
private void add(T t,Node<T> n){
if (n == null) {
n = new Node(t);
return;
}
if (compare(t,n.item)){
if (n.rightChild == null)
n.rightChild = new Node(t);
else add(t,n.rightChild);
}else{
if (n.leftChild == null)
n.leftChild = new Node(t);
else add(t,n.leftChild);
}
}
public boolean compare(T a,T b){
if (a instanceof Integer)
return (int)a > (int)b;
return false;
}
//前序遍历
public T[] prePrint(T[] ts){
System.out.println("根节点:"+getRoot());
System.out.println("前序遍历:");
prePrint(root,ts);
return ts;
}
private void prePrint(Node<T> n,T[] arr){
if (n != null){
arr[i] = n.item;
i++;
}else return;
if (n.leftChild != null)
prePrint(n.leftChild,arr);
if (n.rightChild != null)
prePrint(n.rightChild, arr);
}
}
七、二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
百度百科
二叉搜索树,即树的结构按照左孩子的值 < 根节点的值 < 右孩子结点的值进行排列后的二叉树。一颗二叉搜索树,一个节点的值均大于左边节点的值,小于右边节点的值
-
别名,以下名字指的都是二叉搜索树
-
二叉搜索树
-
二叉查找树
-
二叉排序树
-
-
二叉搜索树的优点
-
可以像链表一样快速的插入和删除
-
可以像数组一样快速的查找
-
-
二叉搜索树的实现
上面二叉树代码的实现即为二叉搜索树的实现
二叉搜索树在存值时进行位置判断,存到合适的位置
八、平衡二叉树
-
平衡树
平衡树(Balance Tree,BT) 指的是,任意节点的子树的高度差都小于等于1。
-
平衡二叉树
或者称为AVL树,平衡二叉树具有以下性质
它是一颗空树,或它左右两颗子树的高度差不超过1,并且它的左右子树也是一颗平衡二叉树
平衡二叉树以二叉搜索树为前提,其大部分操作和原理与二叉搜索树相似,主要的不同在于,插入和删除元素可能会打破树原有的平 衡,平衡二叉树会重新建立一种平衡,而二叉搜索树只关心左孩子的值 < 根节点的值 < 右孩子结点的值
的关系
-
平衡二叉树的优点
-
平衡二叉树降低了二叉搜素树的深度,从而提升了查找效率
-
因为平衡二叉树左右子树节点个数基本相同,所以查找一个元素时就相当于是二分查找,因此时间复杂度为O (logn)
-
-
平衡二叉树的失衡情况
平衡二叉树的失衡一定发生在所操作的结点到根节点的路径上
以下关于平衡二叉树的调整内容来自zhangbaochong的博客
把需要重新平衡的结点叫做α,由于任意两个结点最多只有两个儿子,因此高度不平衡时,α结点的两颗子树的高度相差2.容易看出,这种不平衡可能出现在下面4中情况中:
1.对α的左儿子的左子树进行一次插入
2.对α的左儿子的右子树进行一次插入
3.对α的右儿子的左子树进行一次插入
4.对α的右儿子的右子树进行一次插入
情形1和情形4是关于α的镜像对称,二情形2和情形3也是关于α的镜像对称,因此理论上看只有两种情况,但编程的角度看还是四种情形。
第一种情况是插入发生在“外边”的情形(左左或右右),该情况可以通过一次单旋转完成调整;第二种情况是插入发生在“内部”的情形(左右或右左),这种情况比较复杂,需要通过双旋转来调整。
调整措施:
一、单旋转
上图是左左的情况,k2结点不满足平衡性,它的左子树k1比右子树z深两层,k1子树中更深的是k1的左子树x,因此属于左左情况。
为了恢复平衡,我们把x上移一层,并把z下移一层,但此时实际已经超出了AVL树的性质要求。为此,重新安排结点以形成一颗等价的树。为使树恢复平衡,我们把k2变成这棵树的根节点,因为k2大于k1,把k2置于k1的右子树上,而原本在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。
这种情况称为单旋转。
二、双旋转
对于左右和右左两种情况,单旋转不能解决问题,要经过两次旋转。
对于上图情况,为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树。
-
代码实现(代码写的比较笨,仅是个人实现,没有实际意义)
public class BalanceTree<T> { //封装结点元素的内部类 private static class Node<T> extends tree.Node { T item;//存储结点内容 Node<T> parent;//父节点 Node<T> leftChild;//左孩子 Node<T> rightChild;//右孩子 int bal;//平衡值,添加元素时才计算相应结点,它的值是左子树高减右子树高 //2表示左子树比右子树高2,-2表示右子树比左子树高2 ...省略了一些构造方法... } //根节点 private Node<T> root; //元素个数 private int size; //返回根节点 public Node<T> getRoot(){ return this.root; } //返回元素个数 public int size(){ return this.size; } //添加元素 public void add(T t){ //创建要添加的结点 Node<T> tnNode = new Node<>(t); //树为空则添加到根节点 if (root == null) root = tnNode; //add(tnNode,root)方法寻找添加位置 else add(tnNode,root); size++; } private void add(Node<T> tNode,Node<T> n){ //取出要添加的值,用于比较定位 T t = tNode.item; //比较,寻找添加的位置 if (compare(t,n.item)) { if ( n.rightChild == null) { n.rightChild = tNode; tNode.parent = n; return; } else add(tNode,n.rightChild); } else { if (n.leftChild == null) { n.leftChild = tNode; tNode.parent = n; return; }else add(tNode,n.leftChild); } //isBalance(tNode)判断添加完元素后 //树是否平衡 //平衡返回null,不平衡返回最小不平衡树的根节点 Node<T> unbalNode = isBalance(tNode); //如果不平衡就进行调整 if (unbalNode != null){ if (unbalNode.bal <= -2){ if (unbalNode.rightChild.bal <= 0) //左旋转 leftRotate(unbalNode); else { //右旋左旋 rightRotate(unbalNode.rightChild); leftRotate(unbalNode); } } if (unbalNode.bal >= 2){ if (unbalNode.leftChild.bal >= 0) //右旋转 rightRotate(unbalNode); else { //左旋右旋 leftRotate(unbalNode.leftChild); rightRotate(unbalNode); } } } } //简单的比较,只实现了整型的比较 private boolean compare(T t1,T t2){ if (t1 instanceof Integer && t2 instanceof Integer) return (int)t1 >= (int)t2; return false; } //判断树是否平衡,返回最小不平衡树的根节点 private Node<T> isBalance(Node node){ if (node == null) return null; Node par = node; int balance = 0; while (balance > -2 && balance < 2){ par = par.parent; if (par == null) break; balance = calBalance(par); par.bal = balance; } return par; } private int calBalance(Node<T> node){ if (node == null) return 0; return calHeight(node.leftChild) - calHeight(node.rightChild); } private int calHeight(Node<T> node){ if (node == null) return 0; return 1 + Math.max(calHeight(node.leftChild),calHeight(node.rightChild)); } //平衡调节方式 //右旋转 private void rightRotate(Node<T> node){ Node<T> left = node.leftChild; if (node == root){ left.parent = null; root = left; }else{ left.parent = node.parent; if (node == node.parent.leftChild) node.parent.leftChild = left; else node.parent.rightChild = left; } if (node.leftChild != null) node.leftChild = left.rightChild; left.rightChild = node; node.parent = left; } //左旋转 private void leftRotate(Node<T> node){ Node<T> right = node.rightChild; if (node == root){ right.parent = null; root = right; } else { right.parent = node.parent; if (node == node.parent.rightChild) node.parent.rightChild = right; else node.parent.leftChild = right; } if (node.rightChild != null) node.rightChild = right.leftChild; right.leftChild = node; node.parent = right; } }