Java学习——Java常见数据结构
文章目录
一、数据结构
数据存储的常用结构有:栈、队列、数组、链表和二叉树。
(1). 栈
栈:stack,又称堆栈, 栈(stack)是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为先进后出的线性表。
简单的说:采用该结构的集合,对元素的存取有如下的特点
先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。
栈的入口、出口的都是栈的顶端位置。
进栈与出栈过程图(先进后出):
压栈:存元素。
弹栈:取元素。
(2). 队列
队列:queue,简称队, 队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的一端进行插入,而在另一端进行删除元素的线性表。
队尾(rear)是允许插入的一端。队头(front)是允许删除的一端。空队列是不含元素的空表。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
- 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
- 队列的入口、出口各占一侧。
例如,下图中的左侧为入口,右侧为出口(先进先出):
(3). 数组
数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
查找元素快:通过索引,可以快速访问指定位置的元素
增删元素慢:
- 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。
- 指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。
数组常用方法:引用
(4). 链表
链表 [Linked List]:链表是由一组不必相连(不必相连:可以连续也可以不连续)的内存结构(节点),按特定的顺序链接在一起的抽象数据类型。
补充: 抽象数据类型(Abstract Data Type [ADT]):表示数学中抽象出来的一些操作的集合。
内存结构:内存中的结构,如:struct、特殊内存块…等等之类;
数组和链表的区别和优缺点:
- 数组是一种连续存储线性结构,元素类型相同,大小相等
数组的优点:
- 存取速度快
数组的缺点:
- 事先必须知道数组的长度
- 插入删除元素很慢
- 空间通常是有限制的
- 需要大块连续的内存块
- 插入删除元素的效率很低
链表是离散存储线性结构
- n 个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点,每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点。
链表优点:
- 空间没有限制
- 插入删除元素很快
链表缺点:
- 存取速度很慢
链表常用的有 3 类: 单链表、双向链表、循环链表。
链表的核心操作集有 3 种:插入、删除、查找(遍历)
3类链表结构总图:
单链表
单链表 [Linked List]:由各个内存结构通过一个 Next 指针链接在一起组成,每一个内存结构都存在后继内存结构(链尾除外),内存结构由数据域和 Next 指针域组成。
单链表实现图示:
解析:
Data 数据 + Next 指针,组成一个单链表的内存结构 ;
第一个内存结构称为 链头,最后一个内存结构称为 链尾;
链尾的 Next 指针设置为 NULL [指向空];
单链表的遍历方向单一(只能从链头一直遍历到链尾)。
单链表操作图:
双向链表
双向链表 [Double Linked List]:由各个内存结构通过指针 Next 和指针 Prev 链接在一起组成,每一个内存结构都存在前驱内存结构和后继内存结构(链头没有前驱,链尾没有后继),内存结构由数据域、Prev 指针域和 Next 指针域组成。
双向链表实现图示:
解析:
Data 数据 + Next 指针 + Prev 指针,组成一个双向链表的内存结构;
第一个内存结构称为链头,最后一个内存结构称为链尾;
链头的 Prev 指针设置为 NULL, 链尾的 Next 指针设置为 NULL;
Prev 指向的内存结构称为 前驱, Next 指向的内存结构称为 后继;
双向链表的遍历是双向的,即如果把从链头的 Next 一直到链尾的[NULL] 遍历方向定义为正向,那么从链尾的 Prev 一直到链头 [NULL ]遍历方向就是反向;
双向链表操作图:
循环链表
单向循环链表 [Circular Linked List] : 由各个内存结构通过一个指针 Next 链接在一起组成,每一个内存结构都存在后继内存结构,内存结构由数据域和 Next 指针域组成。
双向循环链表 [Double Circular Linked List] : 由各个内存结构通过指针 Next 和指针Prev 链接在一起组成,每一个内存结构都存在前驱内存结构和后继内存结构,内存结构由数据域、Prev 指针域和 Next 指针域组成。
循环链表的单向与双向实现图示:
解析:
循环链表分为单向、双向两种;
单向的实现就是在单链表的基础上,把链尾的 Next 指针直接指向链头,形成一个闭环;
双向的实现就是在双向链表的基础上,把链尾的 Next 指针指向链头,再把链头的 Prev 指针指向链尾,形成一个闭环;
循环链表没有链头和链尾的说法,因为是闭环的,所以每一个内存结构都可以充当链头和链尾。
循环链表操作图:
(5). 二叉树
二叉树是树的一种,每个节点最多可具有两个子树,即结点的度最大为 2(结点度:结点拥有的子树数)。
例如:
树的一些概念:
二叉树就是每个节点不能多于有两个儿子,上面的图就是一颗二叉树。
二叉树:二叉查找树(binary search tree)。
定义:当前根节点的左边全部比根节点小,当前根节点的右边全部比根节点大。
可以看出,这对我们来找一个数是非常方便快捷的。
一棵树至少会有一个节点(根节点)
树由节点组成,每个节点的数据结构是这样的:
因此,我们定义树的时候往往是 ->定义节点->节点连接起来就成了树 ,而节点的定义就是:一个数据、两个指针(如果有节点就指向节点、没有节点就指向 null)
二叉树的种类
斜树
所有结点都只有左子树,或者右子树。
左斜树:
满二叉树
所有的分支节点都具有左右节点。
满二叉树:
完全二叉树
若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
完全二叉树与非完全二叉树:
二叉树的一些性质
- 二叉树第 i 层上的结点数目最多为 2^(i-1) (i≥1)
- 深度为 h 的二叉树至多有 2^h-1 个结点(h≥1)
- 包含 n 个结点的二叉树的高度至少为 log2 (n+1)
- 在任意一棵二叉树中,若终端结点的个数为 n0,度为 2 的结点数为 n2,则 n0=n2+1。
二叉树的遍历方式
二叉树的遍历方式,一般分为先序遍历,中序遍历,后序遍历。
先序遍历
- 先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
中序遍历
- 先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
后序遍历
- 先访问左节点,然后访问右节点,最后访问根节点(左->右->根)
图中二叉树的遍历:
先序遍历(根-左-右): 1-2-4-8-9-5-10-3-6-7
中序遍历:(左-根-右): 8-4-9-2-10-5-1-6-3-7
后序遍历(左-右-根): 8-9-4-10-5-2-6-7-3-1
二、单向链表与二叉树的java实现
(1). 单向链表
/*
* 单向链表节点
* */
public class Node {
private Integer data; //结点存放的数据
private Node next; //next指针
public Node(Integer data) {
this.data = data;
}
public Integer getData() {
return data;
}
public void setData(Integer data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
/*
* 单向链表
* */
public class SingleLinkedList {
private Node head; //头结点
private int size; //链表长度
public SingleLinkedList() {
}
//添加一个数据节点
public boolean add(Integer data){
Node node = new Node(data);
if (head == null){ //如果头节点为空
head = node; //将该节点 设置为头节点
}else {
//从头节点开始遍历,知道查找到最后一个不为空的节点
Node temp = head;
while (temp.getNext() != null){
temp = temp.getNext();
}
temp.setNext(node); //添加至最后一个节点的下一个节点
}
size++;
return true;
}
//根据下标插入节点
public boolean add(Integer data,int index){
if (index > size){ //如果输入的下标超出链表长度,产生越界
throw new RuntimeException("越界异常!(IndexOutOfBundException), index:" + index + "此时链表长度:" + size);
}
Node node = new Node(data);
if(index == 0){
node.setNext(head); //将该节点添加到头节点之前
head = node; //将当前节点设为头节点
}else{
Node temp = head;
for (int i = 0;i < index-1;i++){
temp = temp.getNext();
}
//节点插入操作
node.setNext(temp.getNext()); //将添加节点的下一个节点设置为index位置的下一个节点
temp.setNext(node); //将index节点的前一个节点 设置为index位置的上一个节点
}
size++;
return true;
}
//遍历并输出节点
public void print(){
Node node = head;
while(node != null){
System.out.println(node.getData() + " ");
node = node.getNext();
}
}
//默认删除链表最后一个节点
public Integer remove(){
if (head == null) { //如果头节点都为空
throw new RuntimeException("链表为空,不能进行删除操作!");
}
if (head.getNext() == null){ //头节点的下一个节点为空时,删除头节点
Integer data = head.getData();
size--; //链表长度减1
head = null;
return data;
}
//通过断开最后节点的联系从而达到删除最后一个节点的目的
Node tFast = head;
Node tLow = null;
while (tFast.getNext() != null){
tLow = tFast;
tFast = tFast.getNext();
}
size--;
tLow.setNext(null);
return tFast.getData();
}
//通过下标查找并删除该节点
public Integer remove(Integer index){
if (index >= size) { //删除的下标大于链表长度时
throw new RuntimeException("越界异常!(IndexOutOfBundException), index:" + index + "此时链表长度:" + size);
}
if (index == 0){ //头节点的下一个节点为空时,删除头节点
Integer data = head.getData();
size--;
head = null;
return data;
}
//根据下标查找删除节点
Node tFast = head;
Node tLow = null;
for (int i = 0;i < index;i++){
tLow = tFast;
tFast = tFast.getNext();
}
tLow.setNext(tFast.getNext()); //将index的上一个节点与index的下一个节点关联
tFast.setNext(null); //将该index节点断开与下个节点的联系
size--;
return tFast.getData();
}
public Integer get(Integer index){
if(index >= size || index < 0){
throw new RuntimeException("越界异常!(IndexOutOfBundException), index:" + index + "此时链表长度:" + size);
}
Node node = head;
for (int i =0;i < index;i++) {
node = node.getNext();
}
return node.getData();
}
public Node getHead() {
return head;
}
public void setHead(Node head) {
this.head = head;
}
public int getSize() {
return size;
}
}
我们编写一个测试类进行测试:
public class MainTest {
public static void main(String[] args) {
SingleLinkedList list = new SingleLinkedList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(6,4);
System.out.println("输出现在链表存有的所有数据");
list.print();
list.remove(3);
System.out.println("--------remove操作后-------");
list.print();
System.out.println("链表长度:" + list.getSize());
System.out.println("-------输出下标为2的结点的数据---------");
System.out.println(list.get(2));
}
}
结果:
(2).二叉树
/*
* 二叉树
*
* */
public class TreeNode {
private Integer data; //数据
private TreeNode left; //左节点
private TreeNode right; //右节点
public TreeNode(Integer data) {
this.data = data;
}
public Integer getData() {
return data;
}
public void setData(Integer data) {
this.data = data;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
}
/*
* 二叉排序树
* */
public class BinarySortTree {
private TreeNode root; //根节点
/**
* 添加数据
*/
public boolean add(Integer data){
if (root == null){ //当二叉树根结点为空时
this.root = new TreeNode(data); //设为根结点
}else {
TreeNode temp = root;
TreeNode parentNode = null;
while (temp != null){ //通过循环找出添加数据的位置
parentNode = temp; //parentNode代指当前结点temp的父节点
//分情况判断新进结点置于何处
if(data < temp.getData()) { //当添加数据比根结点的小,那就进入根结点的左边
temp = temp.getLeft(); //此时的结点为该结点的左节点(进入左结点当中比较)
if (temp == null) { //当该结点为空时,说明没有可比较对象,直接在此处新建结点
parentNode.setLeft(new TreeNode(data));
return true;
}
}else { //当添加数据不小于根结点的数据时 ,那就进入根结点的右边
temp = temp.getRight();
if (temp == null) {
parentNode.setRight(new TreeNode(data));
return true;
}
}
}
}
return false;
}
/**
* 根据key的数值查找数据
* @return
*/
public TreeNode get(Integer key){
TreeNode temp = root;
while(temp != null){
if(temp.getData() > key){ //当查找数据小于于该结点数据时
temp = temp.getLeft(); //结点向左遍历
}else if(temp.getData() < key){ //当查找数据大于该结点数据时
temp = temp.getRight(); //结点向右遍历
}else {
return temp; //查找成功,返回temp对象
}
}return null;
}
public TreeNode getRoot() {
return root;
}
public void setRoot(TreeNode root) {
this.root = root;
}
}
我们将二叉树的三种遍历方式写在了MainTest.java中
import java.util.LinkedList;
import java.util.Queue;
public class MainTest {
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
TreeNode node7 = new TreeNode(7);
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
node3.setRight(node7);
System.out.println("--------广度遍历--------");
printByLayer(root);
System.out.println("--------深度遍历--------");
System.out.println("先序遍历:");
pre_print(root);
System.out.println("中序遍历:");
mid_print(root);
System.out.println("后序遍历:");
last_print(root);
}
/*
* 二叉树 深度遍历
*
* */
//先序遍历: 根-左-右
public static void pre_print(TreeNode root){
if (root != null) { //当当前结点不为空时,停止遍历
//根结点
System.out.println(root.getData());
//左结点
if (root.getLeft() != null) {
pre_print(root.getLeft()); //将该结点设为根节点递归调用,(利用递归实现先序遍历的实现方法)
}
//右节点
if (root.getRight() != null) {
pre_print(root.getRight());
}
}
}
//中序遍历: 左-根-右
public static void mid_print(TreeNode root) {
if (root != null) {
//输出左孩子
if (root.getLeft() != null) { //同理,利用递归
mid_print(root.getLeft());
}
//输出根节点
System.out.println(root.getData());
//输出右孩子
if (root.getRight() != null) {
mid_print(root.getRight());
}
}
}
//后序遍历: 左-右-根
public static void last_print(TreeNode root){
if (root != null) {
//输出左孩子
if (root.getLeft() != null) { //同理,利用递归
last_print(root.getLeft());
}
//输出右孩子
if (root.getRight() != null) {
last_print(root.getRight());
}
//输出根节点
System.out.println(root.getData());
}
}
/*
* 二叉树广度遍历,按层遍历
*
* */
public static void printByLayer(TreeNode root){
if(root == null){
return ;
}
//创建一个队列 存储树结点
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root); //先存入根结点
while (!queue.isEmpty()){
//将队列的数据弹出,存入树结点中
TreeNode temp = queue.poll();
System.out.println(temp.getData());
//管理该结点的左右孩子,按照先进先出顺序存入队列
if(temp.getLeft() != null){
queue.add(temp.getLeft());
}if (temp.getRight() != null){
queue.add(temp.getRight());
}
}
}
}
MainTest.java运行结果:
再写一个测试类Test.java对二叉树TreeNode.java类进行测试:
//import MainTest.java中的中序遍历方法
public class Test {
public static void main(String[] args) {
BinarySortTree tree = new BinarySortTree();
tree.add(14);
tree.add(25);
tree.add(10);
tree.add(18);
tree.add(1);
tree.add(15);
tree.add(29);
mid_print(tree.getRoot()); //调用中序遍历输出二叉树
TreeNode node = tree.get(18);
if (node != null){
System.out.println("查找成功:" + node.getData());
}else
System.out.println("查找失败!");
}
}