数据结构和算法
编程好比是一台笔记本电脑,数据结构和算法就是电脑的内部的CPU,内存。
学习数据结构,可以让我们明白数据在计算机中的存储的方式,从而可以选择更加合理的结构来存储数据,从而提高程序的执行效率,节约资源。
数据结构和算法的重要性
- 算法是程序的灵魂,优秀的程序可以在海量数据计算时,依然保持高速计算
- 一般来说,程序会使用内存计算框架和缓存技术来优化程序。核心框架的主要功能是通过算法来实现
数据结构和算法的关系
数据结构是一门研究组织数据方式的学科,在编程语言中,使用良好的结构可以大幅度提升程序的效率
要学好数据结构 就需要多考虑如何将生活中遇到问题 用程序思维区解决解决
程序= 数据结构 + 算法
数据结构是算法的基础
public static void main(String[] args) {
String str = "Java ,Java ,hello , world";
String newStr = str.replaceAll("Java","中国");//算法
System.out.println(newStr);
}
基本的概念和专业术语
- 数据(data):所有能输入到计算机中的描述客观事物的符号
- 数据元素(data element) 数据的基本单位也称为结点或记录
- 数据项(data item) 有独立含义的数据的最小单位 也称为域
三者之间的关系: 数据> 数据元素> 数据项
- 数据对象 相同特性的数据元素的集合 是数据的一个子集
整数数据对象 N = {0,1,2,3,4,…}
学生数据对象 学生记录的集合 :学生表 > 个人记录>学号 姓名,年龄
- 数据结构(data Structure) 是相互存在的数据元素之间通过一种或多种特定关系组织在一起的集合
数据结构是带“结构”的数据元素的集合,“结构”就是值数据元素之间存在的关系
数据的逻辑结构
数据元素间的这种抽象关系,是与存储无关的 是从具体问题抽象出来的数学模型
划分方法一:
1. 线性结构
线性结构作为最常见一种数据结构,特点是数据元素之间存在一对一的线性关系
线性结构有两种不同的存储结构:
-
顺序的存储结构:顺序表 存储的元素是连续的
-
链式存储结构:链表 链表中的元素不一定是连续的。元素结点中存放数据元素和相邻元素的地址信息
线性结构常见:数组 栈 队列 链表
2. 非线性结构
二维数组 多维数组 树结构 图结构
数据的存储结构
存储结构就是物理结构,数据元素以及相互的关系在计算机中存储的方式
顺序存储结构-- 借助于元素在存储容器中的相对位置来表示元素间的逻辑关系
链式存储结构–借助于元素存储地址的指针表示元素间的逻辑关系
常见的数据结构
线性结构
数组
数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。
特点:
-
查找元素快:通过索引,可以快速访问指定位置的元素
-
增删元素慢
-
指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。如下图
-
指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图
栈(stack)
- 栈:stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。
栈结构的特点:
- 对于元素的操作只能从一端开始
- 先进后出,后进先出
- 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
- 弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
栈的常见操作:以下 方法也是栈操作的规范
-
入栈
-
出栈
-
判断栈是否为空
-
判断栈是否已满
-
返回栈顶元素
-
返回元素在栈中的位置
-
返回栈的实际长度
-
返回栈的容量
-
打印栈
栈接口
public interface IStack {
//1 入栈
boolean push(Object obj);
// 2 出栈
Object pop();
// 3 判断栈是否为空
boolean isEmpty();
// 4 判断栈是否已满
boolean isFull();
// 5 返回栈顶元素
Object peek();
// 6 返回元素在栈中的位置
int getIndex(Object obj);
// 7 返回栈的实际长度
int size();
// 8 返回栈的容量
int getStatckSize();
// 9 打印栈
void display();
}
栈的实现:
用数组模拟栈的结构
public class StackImpl implements IStack {
//定义栈的属性
private Object[] data = null;
private int top = -1;// 栈顶
private int maxSize = 0 ;// 表示栈的容量
public StackImpl(){// 默认初始化一个容量为10的栈
this(10);
}
// 创建一个栈结构 并对栈进行初始化
public StackImpl(int initialSize){
if(initialSize >= 0){
this.data = new Object[initialSize];
this.maxSize = initialSize;
this.top=-1;
}
}
@Override
public boolean push(Object obj) {
if(isFull()){//如果栈已满
System.out.println("栈已满,入栈失败!");
return false;
}
top++;// 更新栈顶指针
data[top] = obj;//将元素保存到栈中
return true;
}
@Override
public Object pop() {
if(isEmpty()){
System.out.println("栈为空,没有可出栈的元素");
return null;
}
Object topObj = data[top];// 获取栈顶元素
top--;
return topObj;
}
@Override
public boolean isEmpty() {
return top == -1 ? true:false;
}
@Override
public boolean isFull() {
return top >= maxSize -1? true:false;
}
@Override
public Object peek() {
if(isEmpty()){
System.out.println("栈为空,没有可出栈的元素");
return null;
}
return data[top];
}
@Override
public int getIndex(Object obj) {
// 需要不断的获取栈顶和目标元素进行比较
while(top != -1){
if( peek().equals(obj)){
return top;
}
top--;
}
return -1;// 表示元素不存在
}
@Override
public int size() {
return 0;
}
@Override
public int getStatckSize() {
return this.top +1;
}
@Override
public void display() {
while(top != -1){
System.out.println(data[top]);
top--;
}
}
public static void main(String[] args) {
// 创建一个栈
IStack stack = new StackImpl();
// 入栈
for(int i = 0 ; i < 10 ;i++){
stack.push(i);
}
stack.push("aa");
stack.display();
}
}
练习:利用栈实现字符串逆序
思路:将字符串转换为字符数组 在将字符数组中的字符逐个入栈, 然后再出栈
队列
-
队列:queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
-
先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
-
队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。
实现:
解决假溢出:
这种队列称为循环队列。
如何判断循环队列究竟是空还是满:
方法一:
设置一个标志flag 初始的时候 falg = 0 ;每当入队一次 就让flage =1;每当出队一次 就让flag = 0;则队列为空的判断条件:flag == 0 && front == rear;队列为满的条件: flag ==1 && front == rear
方法二: 预留一个元素的存储空间,此时 队列未满的判断条件 (rear + 1 )% maxSize == front
队列为空 front == rear
方法三: 设计一个计数器 count 统计队列中元素的个数
队列满的条件 cout > 0 && front==rear
队列为空的条件: count == 0
入队:rear = (rear +1) % maxSize
出队 : front = (front + 1) % maxSize
使用方式三实现循环队列
public interface IQueue {
//入队
void append(Object obj);
// 出队
Object delete();
//获得对头元素
Object getFront();
// 判断队列是否为空
boolean isEmpty();
}
public class CircleQueue implements IQueue {
// 定义队列相关的成员属性
int front;//对头
int rear;//队尾
int count;//统计元素个数
int maxSize;//队列的最大长度
Object[] queue;//队列
public CircleQueue(int initalSize){
front=rear=0;
count = 0;
maxSize = initalSize;
queue = new Object[initalSize];
}
public CircleQueue(){
this(10);
}
@Override
public void append(Object obj) throws Exception {
if(count>0 && front == rear){
throw new Exception("队列已满 ,入队失败");
}
queue[rear] = obj;
rear = (rear +1 )% maxSize;
count++;
}
@Override
public Object delete() throws Exception {
if(isEmpty()){
throw new Exception("队列为空 ,入队失败");
}
Object obj = queue[front];
front = (front+1)%maxSize;
count--;
return obj;
}
@Override
public Object getFront() throws Exception {
if(!isEmpty()){
Object obj = queue[front];
return obj;
}else{
return null;
}
}
@Override
public boolean isEmpty() {
return count==0;
}
}
测试
public static void main(String[] args) throws Exception {
CircleQueue queue = new CircleQueue(5);
queue.append("a");
queue.append("b");
queue.append("c");
queue.append("d");
queue.append("e");
//queue.append("f");
Object o1= queue.delete();
Object o2= queue.delete();
// Object o3= queue.delete();
// Object o4= queue.delete();
System.out.println(o1);
System.out.println(o2);
queue.append("g");
System.out.println("---------------");
while(!queue.isEmpty()){
System.out.println(queue.delete());
}
}
链表
链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表是一种线性表,但是并不会按照线性的顺序存储数据,而是在每一个结点里存放下一个结点的指针。
特点:
-
多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。
-
查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素
-
增删元素快:
- 增加元素:只需要修改连接下个元素的地址即可。
- 删除元素:只需要修改连接下个元素的地址即可。
单向链表
单向链表 一个单链表的结点分为两部分,第一部分保存结点的数据,第二部分保存下一个结点的地址。最后一个节点存储地址的部分指向空值。
单向链表只可以向一个方向遍历,查找一个结点的时候,都需要从一个节点开始每次访问下一个结点,一直到需要的位置
public class SingleLinkedList {
// 链表的结点的个数
private int size;
//头结点
private Node head;
public SingleLinkedList(){
size = 0;a
head= null;
}
// 链表的结点类
private class Node{
private Object data;//结点的数据域
private Node next;//下一个结点的指针
public Node(Object data){
this.data = data;
}
}
// 给链表添加元素 C->B->A
public Object addHead(Object data){
Node newHead = new Node(data);// 创建一个新的结点
if(size == 0 ){
head = newHead;
}else{
newHead.next = head;//新结点的指针指向原来的头结点
head = newHead;//头结点指向新的结点
}
size++;
return data;
}
// 删除结点 在链表头不删除结点
public Object deleteHead(){
Object obj = head.data;
head = head.next;
size--;
return obj;
}
//在链表中查找指定的元素,找到了返回结点Node,没找到则返回null
public Node find(Object data){
Node current = head;//定义一个指针,指向当前比较的结点
int tempSize = size;// 获取当前链表中元素的个数
while(tempSize > 0 ){
if(data.equals(current.data)){
return current;
}else{
current = current.next;
}
tempSize--;
}
return null;
}
// 删除指定的元素 删除成功返回true 否则返回false
public boolean delete(Object data){
if(size == 0 ){
return false;
}
// 获取删除结点的前一个结点和他的后一个结点
Node current = head;
Node previous = head;
while(!current.data.equals(data)){
if (current.next == null){// 判断是否到达链表的结尾
return false;
}else{// 当前结点不是我们要删除的结点 并且没有到达结点的末尾
previous = current;//移动前一个结点的指针
current = current.next;//移动当前结点的指针指向下一个
}
}
// 此时表明找到了要删除的结点, 判断 删除的结点是否是头结点
if(current == head){
head = current.next;
size--;
}else{//不是头结点
previous.next =current.next;
size--;
}
return true;
}
// 判断结点是否为空
public boolean isEmpty(){
return size==0;
}
// 遍历链表
public void display(){
if(size > 0 ){
Node node = head;
int tempSize = size;
if(tempSize == 1){// 如果当前链表只有一个头结点
System.out.println("[" + node.data+"]");
return;
}
while(tempSize >0 ){
if(node.equals(head)){
System.out.print("[" + node.data+"->");
}else if(node.next == null){
System.out.print( node.data+"]");
}else{
System.out.print(node.data+"->");
}
node= node.next;
tempSize--;
}
System.out.println();
}else{// 链表没有结点
System.out.println("[]");
}
}
}
测试
public class SingleLinkListTest {
public static void main(String[] args) {
SingleLinkedList sls = new SingleLinkedList();
sls.addHead("A");
sls.addHead("B");
sls.addHead("C");
sls.addHead("D");
sls.display();
//测试删除元素 删除C
sls.delete("C");
sls.display();
//查找B
System.out.println(sls.find("C"));
}
}
双向链表
双向链表的
循环链表:
栈 队列 链表的区别
栈 队列是线性表,操作受限。
区别:在于运算规则不同
链表和数组:
-
占用的内存空间:链表存放的内存空间可以是连续的,也可以是不连续的,数组一定是连续的。
-
长度的可变性:链表的长度可以根据实际需要 自定伸缩。数组长度一旦定义 则不能改变。
-
数据的访问:
链表的访问:需要移动访问,访问不便捷
数组的访问:数组方问更加便捷
-
使用场景:
数组更加适合数据量大 ,而且需要进行频繁的访问元素
链表更加适合删除,插入等操作
栈的特点:
- 逻辑结构:一对一
- 存储结果:顺序栈 链栈
- 运算规则:后进先出 先进后出
队列的特点:
- 逻辑结构:一对一
- 存储结果:顺序队 链队
- 运算规则:先进先出
链表的特点:
- 逻辑结构:一对一
- 存储结果:顺序表 链表
- 运算规则:随机 顺序存放
非线性结构
树结构
二叉树
- 二叉树:binary tree ,是每个结点不超过2的有序树(tree) 。
每一个元素称为结点
A 根节点
B C D 父节点
叶子结点: 没有子节点的节点(H E F G)
树的高度:最大的层数
子树
森林 多颗子树构成森林
每个结点最多只有两个子节点的形式的树称为二叉树
如果所有的二叉树的叶子结点都在最后一次。节点数 = 2^n - 1 n就是他的层数 将这样的二叉树称为满二叉树
二叉树的遍历
前序遍历:先输出父节点 再遍历左子树和右子树
中序遍历:先遍历左子树 在输出父节点 最后遍历右子树
后序遍历:先遍历左子树 再遍历右子树 最后输出父节点
层序遍历:按照层级逐层遍历
红黑树
红黑树本身就是一颗二叉查找树,将结点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。
红黑树是一种含有红黑结点并能自平衡的二叉树。他必须满足一下的性质:
红黑树的约束:
- 节点可以是红色的或者黑色的
- 根节点是黑色的
- 叶子节点(特指空节点)是黑色的
- 每个红色节点的子节点都是黑色的
- 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同
红黑树的特点:
速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍