数据结构概念(逻辑结构和物理结构)
数据结构概念:把数据元素按照一定的关系组织起来的集合,用来组织和存储数据
数据结构分类:数据结构分为逻辑结构和物理结构两大类
逻辑结构分类:集合结构、线性结构、树形结构、图形结构
a.集合结构:集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系
b.线性结构:线性结构中的数据元素之间存在一对一的关系
c.树形结构:树形结构中的数据元素之间存在一对多的层次关系
物理结构分类:逻辑结构在计算机中真正的表示方式(又称为映像)称为物理结构,也可以叫做存储结构。常见的物理结构有顺序存储结构、链式存储结构。
a.顺序存储结构:把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的 ,比如我们常用的数组就是顺序存储结构。
b.链式存储结构:是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据元素之间并不能反映元素间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置
顺序存储结构--查询块,增删慢
链式存储结构--查询慢,增删块
线性表
顺序表
- 顺序表的查找效率很高,根据索引可以直接定位,查找的时间复杂度为O(1),但是插入和删除的时候需要移动其他的元素,所以效率较低,比如,在位置i处插入,需要把i位置后面的元素依次向右移动一次,因此插入和删除的时间复杂度为O(n)。
- 由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作,需要更换新的更大的数组,把数据从就旧的数组中拷贝到新的数组中,这个过程比较耗时
- Java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。
链表
![](https://i-blog.csdnimg.cn/blog_migrate/ca1f6211ad526042aa777f52fd68bbf0.png)
在使用链表的时候,需要先创建一个节点类
public class Node<T> {
//存储元素
public T item;
//指向下一个结点
public Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
生成链表:
public static void main(String[] args) throws Exception {
//构建结点
Node<Integer> first = new Node<Integer>(11, null);
Node<Integer> second = new Node<Integer>(13, null);
Node<Integer> third = new Node<Integer>(12, null);
Node<Integer> fourth = new Node<Integer>(8, null);
Node<Integer> fifth = new Node<Integer>(9, null);
//生成链表
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
}
栈
既可以使用顺序表实现栈,也可以用链表实现栈
使用数组,实现一个简单的不可扩容的栈:
public class stackDemo<m> {
m[] a;
int f=-1;
public stackDemo(int num)
{
a=(m[])new Object[num];
}
public void add(m n)
{
if(f==a.length-1)
{
System.out.println("栈满");
return;
}
f++;
a[f]=n;
}
public m get()
{
if(f==-1)
{
System.out.println("栈空!");
return null;
}
m temp=a[f];
f--;
return temp;
}
public static void main(String[] args) {
stackDemo s=new stackDemo(5);
String[] str=new String[] {"123","g","e","g","w","f","h","r"};
for(int i=0;i<7;i++) s.add(str[i]);
for(int i=0;i<7;i++) System.out.println(s.get());
}
}
队列
同样的,既可以使用顺序表实现队列,也可以用链表实现队列
public class columnDemo <m>{
m[] a;
int b=0;
int e=0;
public columnDemo(int num)
{
a=(m[])new Object[num];
}
public void add(m value)
{
if(e==a.length)
{
System.out.println("队列已满!");
return;
}
a[e]=value;
e++;
}
public m get()
{
if(b==e)
{
System.out.println("队空!");
return null;
}
int n=b;
b++;
return a[n%a.length];
}
public static void main(String[] args) {
columnDemo c=new columnDemo(5);
String[] str=new String[] {"123","g","e","g","w","f","h","r"};
for(int i=0;i<7;i++) c.add(str[i]);
for(int i=0;i<7;i++) System.out.println(c.get());
}
}
树
树的基本定义
二叉树
![](https://i-blog.csdnimg.cn/blog_migrate/b6418ff839bd209b8c35d5e4d70736dc.png)
private class Node<Key,Value>{
//存储键
public Key key;
//存储值
private Value value;
//记录左子结点
public Node left;
//记录右子结点
public Node right;
public Node(Key key, Value value, Node left, Node right) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
}
满二叉树
一个二叉树,如果每一个层的结点树都达到最大值,则这个二叉树就是满二叉树。
![](https://i-blog.csdnimg.cn/blog_migrate/55e872b834cfceb73259b5a92c9f80f2.png)
完全二叉树
![](https://i-blog.csdnimg.cn/blog_migrate/483f10fa3a4a0ff331459747302dc201.png)
二叉树的遍历
- 前序遍历:先访问根结点,然后再访问左子树,最后访问右子树
- 中序遍历:先访问左子树,中间访问根节点,最后访问右子树
- 后序遍历:先访问左子树,再访问右子树,最后访问根节点
![](https://i-blog.csdnimg.cn/blog_migrate/e48bf7d4577d46fd9e96176f6a8b24f1.png)
前序遍历代码:
//使用前序遍历,获取整个树中的所有键
public Queue<Key> preErgodic(){
Queue<Key> keys = new Queue<>();
preErgodic(root,keys);
return keys;
}
//使用前序遍历,把指定树x中的所有键放入到keys队列中
private void preErgodic(Node x,Queue<Key> keys){
if (x==null){
return;
}
//1.把当前结点的key放入到队列中;
keys.enqueue(x.key);
//2.找到当前结点的左子树,如果不为空,递归遍历左子树
if (x.left!=null){
preErgodic(x.left,keys);
}
//3.找到当前结点的右子树,如果不为空,递归遍历右子树
if (x.right!=null){
preErgodic(x.right,keys);
}
}
中序遍历代码:
//使用中序遍历,获取整个树中的所有键
public Queue<Key> midErgodic(){
Queue<Key> keys = new Queue<>();
midErgodic(root,keys);
return keys;
}
//使用中序遍历,把指定树x中的所有键放入到keys队列中
private void midErgodic(Node x,Queue<Key> keys){
if (x==null){
return;
}
//1.找到当前结点的左子树,如果不为空,递归遍历左子树
if (x.left!=null){
midErgodic(x.left,keys);
}
//2.把当前结点的key放入到队列中;
keys.enqueue(x.key);
//3.找到当前结点的右子树,如果不为空,递归遍历右子树
if (x.right!=null){
midErgodic(x.right,keys);
}
}
后序遍历代码:
//使用后序遍历,获取整个树中的所有键
public Queue<Key> afterErgodic(){
Queue<Key> keys = new Queue<>();
afterErgodic(root,keys);
return keys;
}
//使用后序遍历,把指定树x中的所有键放入到keys队列中
private void afterErgodic(Node x,Queue<Key> keys){
if (x==null){
return;
}
//1.找到当前结点的左子树,如果不为空,递归遍历左子树
if (x.left!=null){
afterErgodic(x.left,keys);
}
//2.找到当前结点的右子树,如果不为空,递归遍历右子树
if (x.right!=null){
afterErgodic(x.right,keys);
}
//3.把当前结点的key放入到队列中;
keys.enqueue(x.key);
}
堆
堆的定义
堆的特性
- 它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
- 它通常用数组来实现。具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3,而子结点的子 结点则分别在位置4,5,6和7,以此类推。
如果一个结点的位置为k,则它的父结点的位置为[k/2],而它的两个子结点的位置则分别为2k和2k+1。这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动:从a[k]向上一层,就令k等于k/2,向下一层就令k等于2k或2k+1。
- 每个结点都大于等于它的两个子结点。这里要注意堆中仅仅规定了每个结点大于等于它的两个子结点,但这两个子结点的顺序并没有做规定,跟我们之前学习的二叉查找树是有区别的。
堆排序
堆可以分为大根堆,小根堆
大根堆:每个节点的值都大于或者等于他的左右孩子节点的值
小根堆:每个结点的值都小于或等于其左孩子和右孩子结点的值0
![](https://i-blog.csdnimg.cn/blog_migrate/ea3fb593d467edde3f2460809794a93c.png)
同样,删除元素也需要进行堆调整:
对于大根堆,索引1处的元素,也就是根结点是最大的元素,当我们把根结点的元素删除后,需要有一个新的根结点出现,这时我们可以暂时把堆中最后一个元素放到索引1处,充当根结点,但是它不满足大根堆的有序性需求,这个时候我们就需要通过一些方法,让这个新的根结点放入到合适的位置。
- 构造大根堆;
- 得到堆顶元素,这个值就是最大值;
- 交换堆顶元素和数组中的最后一个元素,此时所有元素中的最大元素已经放到合适的位置;
- 对堆进行调整,重新让除了最后一个元素的剩余元素中的最大值放到堆顶;
- 重复2~4这个步骤,直到堆中剩一个元素为止;
public class HeapSort {
public static void main(String[] args) {
int[] a=new int[]{5,32,4,23,1,8,64,67,35,85,12,123,543,45,23,10};
System.out.println(Arrays.toString(a));
int l=a.length;
for(int i=0;i<a.length-1;i++)
{
for(int p=l-1;p>=0;p--)
{
sort(a,p,l);
}
int temp=a[0];
a[0]=a[l-1];
a[l-1]=temp;
l--;
}
System.out.println(Arrays.toString(a));
}
public static void sort(int[] a,int p,int l)
{
int c=2*p+1;
if(c<l)
{
int rc=c+1;
if(rc<l && a[c]<a[rc]) c=rc;
if(a[p]<a[c])
{
int temp=a[p];
a[p]=a[c];
a[c]=temp;
p=c;
sort(a,p,l);
}
}
}
}