数据结构与常用集合总结
数据结构与常用集合总结
数据结构(英语:data structure)是计算机中存储、组织数据的方式。 数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。 它包含三方面的内容,逻辑关系、存储关系及操作。 不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。
简单来说数据结构(英语:data structure)是数据的组织、管理和存储格式, 其使用目的是为了高效地访问和修改数据。
数据结构主要分为:数组(Array)、栈(Stack)、队列(Queue)、链表(Linked List)、树(Tree)、散列表(也叫哈希表)(Hash)、堆(Heap)、图(Graph)。 数据结构又可以分为线性表(全名为线性存储结构。使用线性表存储数据的方式可以这样理解,即“把所有数据用一根线儿串起来,再存储到物理空间中”,包含一维数组、栈、队列、链表)和非线性表
⚠️数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)
时间复杂度
在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
表示方法
「 大O符号表示法 」,即 T(n) = O(f(n)),在 大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系
常见的时间复杂度量级
- 常数阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O(n²)
- 立方阶O(n³)
- K次方阶O(n^k)
- 指数阶(2^n)
求解算法复杂度一般分以下几个步骤
- 找出算法中的基本语句:算法中执行次数最多的语句就是基本语句,通常是最内层循环的循环体。
- 计算基本语句的执行次数的数量级:只需计算基本语句执行次数的数量级,即只要保证函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,使注意力集中在最重要的一点上:增长率。
- 用大Ο表示算法的时间性能:将基本语句执行次数的数量级放入大Ο记号中。
其中用大O表示法通常有三种规则
- 用常数1取代运行时间中的所有加法常数;
- 只保留时间函数中的最高阶项;
- 如果最高阶项存在,则省去最高阶项前面的系数;
实例分析
下面介绍一下常用的实例有助于我们来理解时间复杂度
1.常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),实例如下
int i = 0;//执行一次
int j = 1;//执行一次
++i;//执行一次
j++;//执行一次
int m = i + j;//执行一次
int n = j - i;//执行一次
复制代码
上述代码在执行的时候,每行代码执行次数都是一次,不会随着问题规模n的变化而变化,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
2.对数阶O(logN)
先上代码再来分析,这里n先是一个不确定的数
int i = 1;//执行一次
while(i<n)
{
i = i * 2;//执行log2^n
}
复制代码
从上面代码可以看出,在while循环里面,每次都将i*2然后重新赋值给i,乘完之后,i距离n会越来越近。假设我们在循环j次之后,i> n了,此时就退出当前的循环,也就是说2的j次方就等于n,那么j=log2^n,也就是说循环log2^n次以后,这个代码就结束了。因此我们得到这段代码的时间复杂度为 T(n)=O(logn)。
3.线性阶O(n)
我们这里用一个经常用的代码来分析
int j = 0;//执行1次
for(i=1; i<=n; i++)//执行n次
{
j = i;//执行n次
j++;//执行n次
}
复制代码
这段代码中for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用T(n)=O(n)来表示它的时间复杂度。
4.线性对数阶O(nlogN)
线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。参考代码如下
for(m=1; m<n; m++)//执行n次
{
i = 1;//执行n次
while(i<n)
{
i = i * 2;//执行n*logN次
}
}
复制代码
首先while循环里面的时间复杂度就是对数阶O(logN),和对数阶O(logN)的实例一样的道理,然后这个while循环会根据外层的for循环的执行会被执行n次,因此就是T(n)=O(nlogN)
5. 平方阶O(n²)
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。典型的就是双重for循环实例
for(i=1; i<=n; i++)//执行n次
{
for(j=1; j<=n; j++)//执行n*n次
{
k = j;//执行n*n次
k++;
}
}
复制代码
这段代码就是嵌套了两层n循环,因此时间复杂度为O(n*n),即 T(n)=O(n²)
如果我们将外层for循环的n改成m,他的时间复杂度是多少了
for(i=1; i<=m; i++)//执行m次
{
for(j=1; j<=n; j++)//执行m*n次
{
k = j;
k++;
}
}
复制代码
这段代码第一层嵌套了m循环,第二层是n循环,因此时间复杂度为O(mn),即 T(n)=O(mn)
6. 立方阶O(n³)
参照上面的平方阶O(n²)的实例,我们能推测出立方阶O(n³)就是嵌套了三层n循环,实例代码如下
for(i=1; i<=n; i++)//执行n次
{
for(j=1; j<=n; j++)//执行n*n次
{
for(k=1; k<=n; k++)//执行n*n*n次
{
o = k;
o++;
}
}
}
复制代码
这段代码就是嵌套了三层n循环,因此时间复杂度为O(nnn),即 T(n)=O(n³)
7. K次方阶O(n^k)
因此也能得到K次方阶O(n^k)就是嵌套了k层n循环,这样就不举例了
8. 指数阶(2^n)
指数阶表现的最常用的场景就是求子集(给定一组 不含重复元素 的整数数组 nums,返回该数组所有可能的子集(幂集)),实例代码如下
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> output = new ArrayList();
output.add(new ArrayList<Integer>());
for (int num : nums) {//执行n次
List<List<Integer>> newSubsets = new ArrayList();
for (List<Integer> curr : output) {//
newSubsets.add(new ArrayList<Integer>(curr){{add(num);}});
}
for (List<Integer> curr : newSubsets) {
output.add(curr);
}
}
return output;
}
复制代码
时间复杂度为T(n)=O(n*2^n)
⚠️ 加法法则:总复杂度等于量级最大的那段代码的复杂度
⚠️ 多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。
空间复杂度
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O. (f(n))。 比如直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。 而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
表示方法
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。空间复杂度 S(n) = O(f(n))
常见的时间复杂度量级
- 常数阶O(1)
- 线性阶O(n)
- 平方阶O(n²)
实例分析
1.常数阶O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
int i = 0;
int j = 1;
++i;
j++;
int m = i + j;
int n = j - i;
复制代码
代码中的 i、j、m、n 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
2.线性阶O(n)
直接上代码来分析
int[] m = new int[n];//这行代码占用的内存大小为n
for(i=1; i<=n; ++i)//下面的循环没有分配新的空间
{
j = i;
j++;
}
复制代码
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
特殊的递归算法的空间复杂度
void fun1(int n){
if(n<=1){
return;
}
fun1(n-1);
}
复制代码
假设我们这里传入参数6,那么fun1(n=6)的调用信息先入栈。接下来递归调用相同的方法,fun1(n=5) 的调用信息入栈。以此类推,递归越来越深,入栈的元素也越来越多。当n=1时,达到递归结束条件,执行return指令,方法出栈。最终,“方法调用栈”的全部元素会一一出栈。 由上面“方法调用栈”的出入栈过程可以看出,执行递归操作所需要的内存空间和递归的深度成正比。纯粹的递归操作的空间复杂度也是线性的,如果递归的深度是n,那么空间复杂度就是O(n),即S(n)=O(n)。
3.平方阶O(n²)
int[][] matrix = new int[n][n];
复制代码
这段代码中二维数组就是n*n,即S(n) = O(n²)
⚠️⚠️⚠️一个算法中,考量一个算法的好坏都是从时间复杂度和空间复杂度上去对比考量的,最少时间最小空间肯定是最好的,有时候要根据具体情况去做时间复杂度和空间复杂度的取舍。在绝大多数时候,时间复杂度更为重要一些,我们宁可多分配一些内存空间,也要提升程序的执行速度。
数组(Array)
数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。数组在内存中是顺序存储的,因此可以很好的实现逻辑上的顺序表。
数组在内存中顺序存储
内存是由一个个连续的内存单元组成的,每一个内存单元都有自己的地址。在 这些内存单元中,有些被其他数据占用了,有些是空闲的。 数组中的每一个元素,都存储在小小的内存单元中,并且元素之间紧密排列, 既不能打乱元素的存储顺序,也不能跳过某个存储单元进行存储。
实例讲解
int[] nums=new int[]{3,1,2,5,4,9,7,2};
复制代码
在上图中,橙色的格子代表空闲的存储单元,灰色的格子代表已占用的存储单元,而红色的连续格子代表数组在内存中的位置。不同类型的数组,每个元素所占的字节个数也不同,本图只是一个简单的示意图。
数组的基本操作
数组的增删改查
- 首先说增:数组中增加新数据得先判断插入的下角标是否超出数组范围,超出的话抛异常; 再判断这个数组的大小是否已满,满的话进行当前数组大小的2倍进行扩容,扩容后进行重新赋值; 然后再是真正的插入了,插入的话,需要将当前位置及其后面位置的数据向后移动一位,然后把新数据插入到当前位置,先移再插。
- 然后再说删:数组的删除的逻辑和增加逻辑相似,只不过是先删再移动
- 然后再说改:改的话,判断一下修改的index位置是否存在,不存在抛异常,存在的话就直接修改赋值
- 最好说查:查的话,判断一下修改的index位置是否存在,不存在抛异常,存在的话就直接查询返回值
我们来根据这个思路来用代码实现一下数组的增删改查
public class DataStructureArray {
/**
* 数组增删改查操作的代码实现
* 3,1,2,
*
* @param args
*/
public static void main(String[] args) {
MyArray myArray = new MyArray(2);
//增
myArray.add(0, 3);
myArray.add(1, 1);
myArray.add(2, 2);
System.out.print("\n增 ");
myArray.outPut();
//删
// myArray.delete(3);//数组越界
myArray.delete(1);
System.out.print("\n删 ");
myArray.outPut();
//改
// myArray.update(2, 1);//数组越界
myArray.update(1, 1);
System.out.print("\n改 ");
myArray.outPut();
//查
System.out.print("\n查 "+myArray.get(0));
}
public static class MyArray {
private int[] myArray;
private int size;
public MyArray(int capacity) {
this.myArray = new int[capacity];
size = 0;
}
/**
* 增
*
* @param index
* @param element
*/
private void add(int index, int element) {
if (0 <= index && index <= size) {
//先判断一下是否需要扩容
if (size >= myArray.length) {
//数组扩容
resize();
}
//从index位置开始移动,从右向左循环,将元素逐个向右移动一位
for (int i = size - 1; i >= index; i--) {
myArray[i + 1] = myArray[i];
}
myArray[index] = element;
size++;
} else {
throw new IndexOutOfBoundsException("超出数组实际元素范围");
}
}
/**
* 数组扩容
*/
private void resize() {
//我们这里采取的是2倍扩容
int[] myNewArray = new int[myArray.length * 2];
//数据copy,旧数组数据复制到新数组上面去
System.arraycopy(myArray, 0, myNewArray, 0, myArray.length);
//再赋值
myArray = myNewArray;
}
/**
* 删
*
* @param index
* @return
*/
private int delete(int index) {
if (0 <= index && index < size) {
//得到删除的元素
int deleteElement = myArray[index];
//然后再将index位置右边元素向左移动一位
for (int i = index; i < size - 1; i++) {
myArray[i] = myArray[i + 1];
}
size--;
return deleteElement;
} else {
throw new IndexOutOfBoundsException("超出数组实际元素范围");
}
}
/**
* 改
*
* @param index
* @param element
*/
private void update(int index, int element) {
if (0 <= index && index < size) {
myArray[index] = element;
} else {
throw new IndexOutOfBoundsException("超出数组实际元素范围");
}
}
/**
* 查
*
* @param index
* @return
*/
private int get(int index) {
if (0 <= index && index < size) {
return myArray[index];
} else {
throw new IndexOutOfBoundsException("超出数组实际元素范围");
}
}
/**
* 输出数据
*/
private void outPut() {
for (int i = 0; i < size; i++) {
System.out.print(myArray[i]);
}
}
}
}
复制代码
数组的优缺点
从代码的实现上可以看出来,数组拥有非常高效的查询能力,给出下角标就能很快的查询到对应元素,说到这里,就要提一下二分查找法,在刷算法的时候会经常用到这个算法思想去解决实际问题;反之,我们从代码实现层面也能看出来数组的增删是很耗时,每一次都要做数据的移动。因此我们总结到数组是适合做改查操作、不适合做增删操作的。
数组实现的常用集合
ArrayList就是一个动态修改的数组,我们可以查看一下它的源码来加深一下对数组的实现的理解 Vector也是一个动态数组,它是同步的,是一个可以改变大小的数组。
链表(Linked List)
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。
链表在内存中的存储
链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位 置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。
和上面数组相同的数据,链表的存储方式如下
链表的基本操作
链表的增删改查
- 首先说增:链表中增加数据,得先判断是不是空链表,是空的话,直接赋值; 插入元素插入的位置是头部,将插入的节点的next指向当前的头节点,然后将头节点设置为当前值 插入元素插入的位置是尾部,将尾部节点的next指向当前节点,然后再设置尾部节点为当前值 插入元素插入的位置是中间,先拿到插入位置的前一个节点,设置插入节点的下一个是当前位置这个节点(即前一个节点的next节点),再设置前一个的节点next是当前插入的
- 然后再说删:链表的删除的逻辑和增加逻辑相似
- 然后再说改:改的话,先查到当前这个节点,然后设置当前节点的值
- 最好说查:查的话,得从头节点开始查,头节点的next,再next,一直查到为止
我们来根据这个思路来用代码实现一下链表的增删改查
public class DataStructureLinkedList {
/**
* 链表增删改查操作的代码实现
* 3,5,7,
* 关联常用数组:LinkedList
*
* @param args
*/
public static void main(String[] args) {
MyLinked myLinked = new MyLinked();
myLinked.add(0, 3);
myLinked.add(1, 5);
myLinked.add(2, 7);
System.out.print("\n增 ");
myLinked.output();
// myLinked.delete(0);
myLinked.delete(1);
// myLinked.delete(2);
System.out.print("\n删 ");
myLinked.output();
myLinked.update(0,9);
System.out.print("\n改 ");
myLinked.output();
}
public static class MyLinked {
private Node head;
private Node last;
private int size;
public class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
/**
* 增
*
* @param index
* @param element
*/
private void add(int index, int element) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node currentNode = new Node(element);
//头部插入
if (index == 0) {
//空链表
if (head == null) {
head = currentNode;
last = currentNode;
//头部插入
} else {
currentNode.next = head;
head = currentNode;
}
//尾部插入
} else if (index == size) {
last.next = currentNode;
last = currentNode;
//中间插入
} else {
Node preNode = get(index - 1);
currentNode.next = preNode.next;
preNode.next = currentNode;
}
size++;
}
/**
* 删
*
* @param index
* @return
*/
private Node delete(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node deleteNode = null;
//头节点删除
if (index == 0) {
deleteNode = head;
head = head.next;
//尾节点删除
} else if (index == size - 1) {
Node preNode = get(index - 1);
deleteNode = last;
preNode.next = null;
last = preNode;
//中间节点删除
} else {
Node preNode = get(index - 1);
preNode.next = preNode.next.next;
deleteNode = preNode.next;
}
size--;
return deleteNode;
}
/**
* 改
*
* @param element
*/
private void update(int index, int element) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node node = get(index);
node.data = element;
}
/**
* 查
*
* @param index
* @return
*/
private Node get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
private void output() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data);
temp = temp.next;
}
}
}
}
复制代码
链表的优缺点
链表是写操作快,读操作慢,实际应用场景中,那些需要频繁增删数据的就适合用链表去实现
链表实现的常用集合
LinkedList就是一个双向链表,我们可以查看一下它的源码来加深一下对链表的实现的理解
⚠️⚠️⚠️说到这里,我们就介绍完了数组和链表,其实️数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。
栈(Stack)
堆栈(英语:stack)又称为栈或堆叠,是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,英语:top)进行加入数据(英语:push)和移除数据(英语:pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。
常与另一种有序的线性资料集合队列相提并论。下面我们也会讲到。
栈就像一个装羽毛球的球筒(一端封闭,另一端开口),往圆筒里放入羽毛球,先放入的靠近圆筒底部,后放入的靠近圆筒入口。这就相当于是一个push入栈的过程。那么,要想取出这些羽毛球,则只能按照和放入顺序相反的顺序来取,先取出 后放入的,再取出先放入的,而不可能把最里面最先放入的羽毛球优先取出。这就相当于一个pop出栈的过程。
栈的基本操作
栈的基本操作只有入栈和出栈,入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶。出栈操作(pop) 就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶。我们接下来分别用数组和链表来实现栈。
数组实现栈
import java.util.Stack;
public class DataStructureUseArrayRealizeStack {
/**
* 这里用数组来实现栈
*
* @param args
*/
public static void main(String[] args) {
MyStack myStack = new MyStack(3);
myStack.push(1);
myStack.push(2);
myStack.push(3);
System.out.print("\n入栈 ");
myStack.output();
// myStack.push(4);//超出栈的范围大小
myStack.pop();
myStack.pop();
myStack.pop();
System.out.print("\n出栈 ");
myStack.output();
// myStack.pop();//栈内无数据
}
public static class MyStack {
private int[] data;
private int size;
private int topIndex;
public MyStack(int size) {
data = new int[size];
this.size = size;
topIndex = -1;
}
private void push(int element) {
if (isFull()) {
throw new IndexOutOfBoundsException("超出栈的范围大小");
} else {
data[topIndex + 1] = element;
topIndex++;
}
}
private int pop() {
if (isEmpty()) {
throw new IndexOutOfBoundsException("栈内无数据");
} else {
int[] newdata = new int[data.length - 1];
for (int i = 0; i < data.length - 1; i++) {
newdata[i] = data[i];
}
int element = data[topIndex];
topIndex--;
data = newdata;
return element;
}
}
private boolean isFull() {
return data.length - 1 == topIndex;
}
private boolean isEmpty() {
return topIndex == -1;
}
private void output() {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]);
}
}
}
}
复制代码
链表实现栈
public class DataStructureUseLinkeListdRealizeStack {
/**
* 用链表去实现栈
* 我们首先要实现链表,然后再根据链表去实现栈
*
* @param args
*/
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.push(3);
System.out.print("\n入栈 ");
myStack.output();
myStack.pop();
myStack.pop();
System.out.print("\n出栈 ");
myStack.output();
myStack.pop();
// myStack.pop();//栈内无数据
}
public static class MyStack {
private LinkedList linkedList;
private MyStack() {
linkedList = new LinkedList();
}
private void push(int element) {
linkedList.addToFirst(element);
}
private int pop() {
if (linkedList.isEmpty()) {
throw new IndexOutOfBoundsException("栈内无数据");
} else {
return linkedList.deleteFirst();
}
}
private void output() {
linkedList.output();
}
/**
* 节点
*/
private class Node {
private int data;
private Node next;
private Node(int data) {
this.data = data;
}
}
/**
* 链表
*/
private class LinkedList {
private Node first;
private LinkedList() {
first = null;
}
private boolean isEmpty() {
return first == null;
}
/**
* @param element
*/
private void addToFirst(int element) {
Node newNode = new Node(element);
newNode.next = first;
first = newNode;
}
private int deleteFirst() {
Node firstNode = first;
first = first.next;
return firstNode.data;
}
private void output() {
Node currentNode = first;
while (currentNode != null) {
System.out.print(currentNode.data);
currentNode = currentNode.next;
}
}
}
}
}
复制代码
栈的特点
优点:由于栈中存放数据的结构是后放进去的数据先取出来(后进先出),针对一些操作需要取最新数据时,选择栈作为数据结构是最合适的。
缺点:访问栈中的任意数据时,就需要从最新的数据开始取,效率较低。
栈实现的常用集合
Stack是Vector的一个子类,它实现了一个标准的后进先出的栈。
队列(Queue)
队列,又称为伫列(queue),计算机科学中的一种抽象资料型别,是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。
队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。
队列就像公路上有一条单行隧道,所有通过隧道的车辆只允许从隧道入口驶入,从隧道出口驶出,不允许逆行。因此,要想让车辆驶出隧道,只能按照它们驶入隧道的顺序,先驶入的车辆先驶出,后驶入的车辆后驶出,任何车辆都无法跳过它前面的车辆提前驶出。
队列的基本操作
队列的基本操作只要入队和出队。入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素, 新元素的下一个位置将会成为新的队尾。出队操作(dequeue) 就是把元素移出队列,只允许在队头一侧移出元素,出队元素的后一个元素将会成为新的队头。队列也是数组和链表都能实现队列的这种操作,我们也分别从数组和链表两种方案来实现这个队列。
数组实现队列
public class DataStructureUseArrayRealizeQueue {
/**
* 这里用数组来实现队列(循环队列)
*
* @param args
*/
public static void main(String[] args) {
MyQueue myQueue = new MyQueue(4);
myQueue.enQueue(1);
myQueue.enQueue(2);
myQueue.enQueue(3);
myQueue.enQueue(4);
System.out.print("\n入队 ");
myQueue.output();
// myQueue.enQueue(5);// 超出队列的长度
myQueue.deQueue();
myQueue.deQueue();
System.out.print("\n出队 ");
myQueue.output();
myQueue.enQueue(6);
System.out.print("\n再入队 ");
myQueue.output();
myQueue.deQueue();
myQueue.deQueue();
myQueue.deQueue();
// myQueue.deQueue();// 队列已空
}
public static class MyQueue {
private int[] data;
private int front;//队头
private int rear;//队尾
private MyQueue(int capacity) {
this.data = new int[capacity + 1];//队尾指针指向的位置永远空出1位,所以队列最大容量比数组长度小 1。因此我们这里实现的时候设置数组长度的时候用capacity + 1
front = rear = 0;
}
/**
* 入队
*
* @param element
*/
private void enQueue(int element) {
if (isFull()) {
throw new IndexOutOfBoundsException("超出队列的长度");
} else {
data[rear] = element;
rear = (rear + 1) % data.length;//这里是循环队列,所以不是直接rear++,而是通过数组的循环找到下一个队尾下角标
}
}
/**
* 队列满了,队尾下标与数组长度相除取余数和队头下标是否相等来判断是不是队列已满
*
* @return
*/
private boolean isFull() {
return (rear + 1) % data.length == front;
}
/**
* 出队
*/
private int deQueue() {
if (isEmpty()) {
throw new IndexOutOfBoundsException("队列已空");
} else {
int element = data[front];
front = (front + 1) % data.length;
return element;
}
}
/**
* 空
*
* @return
*/
private boolean isEmpty() {
return front == rear;
}
private void output() {
//从头开始,这里的累加是循环的
for (int i = front; i != rear; i = (i + 1) % data.length) {
System.out.print(data[i]);
}
}
}
}
复制代码
链表实现队列
public class DataStructureUseLinkeListdRealizeQueue {
/**
* 用链表去实现队列
*
* @param args
*/
public static void main(String[] args) {
MyQueue myQueue = new MyQueue();
myQueue.enQueue(1);
myQueue.enQueue(2);
myQueue.enQueue(3);
myQueue.enQueue(4);
System.out.print("\n入队 ");
myQueue.output();
myQueue.deQueue();
myQueue.deQueue();
System.out.print("\n出队 ");
myQueue.output();
myQueue.enQueue(6);
System.out.print("\n再入队 ");
myQueue.output();
myQueue.deQueue();
myQueue.deQueue();
myQueue.deQueue();
System.out.print("\n再出队 ");
myQueue.output();
}
public static class MyQueue {
private Node head;
private Node rear;
private int size;
private MyQueue() {
head = null;
rear = null;
size = 0;
}
private void enQueue(int element) {
Node newNode = new Node(element);
if (isEmpty()) {
head = newNode;
} else {
rear.next = newNode;
}
rear = newNode;
size++;
}
private boolean isEmpty() {
return head == null;
}
private int deQueue() {
if (isEmpty()) {
throw new NullPointerException("队列无数据");
} else {
Node node = head;
head = head.next;
size--;
return node.data;
}
}
private void output() {
Node node = head;
while (node != null) {
System.out.print(node.data);
node = node.next;
}
}
/**
* 节点
*/
private class Node {
private int data;
private Node next;
private Node(int data) {
this.data = data;
}
}
}
}
复制代码
队列的特点
队列是一种比较特殊的线性结构。它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中最先插入的元素也将最先被删除,对应的最后插入的元素将最后被删除。因此队列又称为“先进先出”(FIFO—first in first out)的线性表,与栈(FILO-first in last out)刚好相反。
队列实现的常用集合
- 双端队列:Deque
- 未实现阻塞接口的:
- LinkedList : 实现了Deque接口,受限的队列
- PriorityQueue : 优先队列,本质维护一个有序列表。可自然排序亦可传递 comparator构造函数实现自定义排序。
- ConcurrentLinkedQueue:基于链表 线程安全的队列。增加删除O(1) 查找O(n)
- 实现阻塞接口的:实现BlockingQueue接口的五个阻塞队列,其特点:线程阻塞时,不是直接添加或者删除元素,而是等到有空间或者元素时,才进行操作。
- ArrayBlockingQueue: 基于数组的有界队列
- LinkedBlockingQueue: 基于链表的无界队列
- PriorityBlockingQueue:基于优先次序的无界队列
- DelayQueue:基于时间优先级的队列
- SynchronousQueue:内部没有容器的队列 较特别 --其独有的线程一一配对通信机制
队列主要任务场景就是任务的调度管控、处理并发任务
散列表(也叫哈希表)(Hash)
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名{\displaystyle x}x到首字母{\displaystyle F(x)}F(x) 的一个函数关系),在首字母为W的表中查找“王”姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母”是这个例子中散列函数的函数法则{\displaystyle F()}F() ,存放首字母的表对应散列表。关键字和函数法则理论上可以任意确定。
散列函数(也叫哈希函数)
散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。[1]好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。
如今,散列算法也被用来加密存在数据库中的密码(password)字符串,由于散列算法所计算出来的散列值(Hash Value)具有不可逆(无法逆向演算回原本的数值)的性质,因此可有效的保护密码。
- 常见的散列函数
- 直接定址法:取关键字或关键字的某个线性函数值为散列地址。即hash(k)=k或hash(k)=a*k+b,其中a,b为常数(这种散列函数叫做自身函数)
- 数字分析法:假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
- 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。
- 随机数法:使用rand()等随机函数构造。
- 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即hash(k)=k mod p,p< =m。不仅可以对关键字直接取模,也可在折叠法、平方取中法等运算之后取模。对p的选择很重要,一般取素数或m,若p选择不好,容易产生冲突。
解决哈希冲突的方法
-
开放地址法:key哈希后,发现该地值已被占用,可对该地址不断加1,直到遇到一个空的地址。
-
再哈希法:发生“碰撞”后,可对key的一部份再进行哈希处理。
-
链地址法:链地址法是通过将key映射在同一地址上的value,做成一个链表
哈希表的基本操作
哈希表的基本操作涉及到增删改查方法,我们一个一个来分析
- 增(put)/改:就是在散列表中插入新的键值对(在JDK中叫作Entry)。例如:我们要调用hash.put("001","张三"),意思就是插入一组key=001.value="张三"的键值对。我们来分析一下具体步骤, 首先我们要通过哈希函数把key=001转化成数组下标index;如果这时候下标index对应的位置没有元素,我们就把这个Entry填充到数组下标index的值的位置; 这时候我们思考一下,因为数组长度有限,当插入的Entry越来越多的时候,不同key做哈希运算得到的下标可能是相同的,当出现相同下标index的时候,我们该怎么处理这种情况了,这就涉及到了哈希冲突,我们上面也列举了解决哈希冲突的方法。HashMap中采用的是链地址法。 这时候我们再思考一下,因为数组在实例化的时候有设置数组长度,再反观一下链表的查询在数据量大的时候就会很慢。当散列表存的键值对越来越多的时候,我们将要考虑扩容,说到扩容就涉及到负载因子( loadFactor负载因子是和扩容机制有关的,意思是如果当前容器的容量,达到了我们设定的最大值,就要开始执行扩容操作。 举个例子来解释,避免小白听不懂: 比如说当前的容器容量是16,负载因子是0.75,16* 0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作。 他的作用很简单,相当于是一个扩容机制的阈值)
- 查(get):读操作就是通过给定的Key,在散列表 中查找对应的Value。例如:我们要调用hash.get("001"),意思是查找Key=001的Entry在散列表中所对应的值。我们也来分析一下具体怎么做 通过哈希函数,把Key转化成数组下标index;找到数组下标index所对应的元素,如果这个元素的Key=001,那么就找到了;如果这个Key不是001也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。
- 删(remove):删除的话也是通过查找key的哈希函数找到对应的下标再进行删除
public class DataStructureHash {
/**
* 这里我们就是简单的实现了一个散列表,具体的全方面实现去参考HashMap
*
* @param args
*/
public static void main(String[] args) {
MyHash myHash = new MyHash();
myHash.put(0, 1);
myHash.put(1, 2);
myHash.get(0);
myHash.remove(0);
}
public static class MyHash {
private static final int DEFAULT_INITAL_CAPACITY = 5;//定义的是默认长度
private static final float LOAD_FACTOR = 0.75f;//负载因子
private Entry[] table = new Entry[DEFAULT_INITAL_CAPACITY];//初始化
private int size = 0;//哈系表大小
private int use = 0;//使用的地址数量
private class Entry {
int key;//关键字
int value;
Entry next;//链表
public Entry(int key, int value, Entry entry)//构造函数
{
this.key = key;
this.value = value;
this.next = entry;
}
}
/**
*
* @param key
* @param value
*/
public void put(int key, int value) {
int index = hash(key);//通过hash方法转换,采用的是直接法
if (table[index] == null)//说明位置未被使用
{
table[index] = new Entry(-1, -1, null);
}
Entry tmp = table[index];
if (tmp.next == null)//说明位置未被使用
{
table[index].next = new Entry(key, value, null);
size++;
use++;
if (use >= table.length * LOAD_FACTOR)//判断是否需要扩容
{
resize();//扩容方法
}
} else {//已被使用,则直接扩展链表
for (tmp = tmp.next; tmp != null; tmp = tmp.next) {
int k = tmp.key;
if (k == key) {
tmp.value = value;
return;
}
}
Entry temp = table[index].next;
Entry newEntry = new Entry(key, value, temp);
table[index].next = newEntry;
size++;
}
}
/**
*
* 删除,链表的中间值删除方法
* @param key
*/
public void remove(int key)
{
int index = hash(key);
Entry e = table[index];
Entry pre = table[index];
if (e != null && e.next != null) {
for (e = e.next; e != null; pre = e, e = e.next) {
int k = e.key;
if (k == key) {
pre.next = e.next;
size--;
return;
}
}
}
}
/**
* 通过key提取value
* @param key
* @return
*/
public int get(int key)
{
int index = hash(key);
Entry e = table[index];
if (e != null && e.next != null) {
for (e = e.next; e != null; e = e.next) {
int k = e.key;
if (k == key) {
return e.value;
}
}
}
return -1;
}
/**
* 返回元素个数
* @return
*/
public int size() {
return size;
}
/**
* 哈希表大小
* @return
*/
public int getLength() {
return table.length;
}
/**
* 扩容
*/
private void resize() {
int newLength = table.length * 2;
Entry[] oldTable = table;
table = new Entry[newLength];
use = 0;
for (int i = 0; i < oldTable.length; i++) {
if (oldTable[i] != null && oldTable[i].next != null) {
Entry e = oldTable[i];
while (null != e.next) {
Entry next = e.next;
int index = hash(next.key);
if (table[index] == null) {
use++;
table[index] = new Entry(-1, -1, null);
}
Entry temp = table[index].next;
Entry newEntry = new Entry(next.key, next.value, temp);
table[index].next = newEntry;
e = next;
}
}
}
}
/**
* 得到key的下标(哈希函数)
* @param key
* @return
*/
private int hash(int key) {
return key % table.length;
}
}
}
复制代码
哈希表的优缺点
优点:哈希表不仅速度快,编程实现也相对容易
缺点:哈希表也有一些缺点它是基于数组的,数组创建后难于扩展某些哈希表被基本填满时,性能下降得非常严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。 而且,也没有一种简便的方法可以以任何一种顺序〔例如从小到大〕遍历表中数据项。
哈希表实现的常用集合
HashMap、HashTable、ConcurrentHashMap
树(Tree)
树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n> 0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
-
每个节点都只有有限个子节点或无子节点;
-
没有父节点的节点称为根节点;
-
每一个非根节点有且只有一个父节点;
-
除了根节点外,每个子节点可以分为多个不相交的子树;
-
树里面没有环路(cycle)
-
树的相关术语
- 节点的度:一个节点含有的子树的个数称为该节点的度;
- 树的度:一棵树中,最大的节点度称为树的度;
- 叶节点或终端节点:度为零的节点;
- 非终端节点或分支节点:度不为零的节点;
- 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
- 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
- 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
常见的树
二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。二叉树节点的两个孩子节点,一个被称为左孩子(left child) ,一个被称为右孩 子(right child)。这两个孩子节点的顺序是固定的,就像人的左手就是左手,右手 就是右手,不能够颠倒或混淆。
满二叉树:一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。
完全二叉树:对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
AVL树:AVL树是最先发明的自平衡二叉查找树。 在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
二叉树包含许多特殊的形式,每一种形式都有自己的作用,但是其最主要的应用还在于进行查找操作和维持相对顺序这两个方面。
二叉树的遍历
从更宏观的角度来看,二叉树的遍历归结为两大类。
- 深度优先遍历(前序遍历[---输出顺序是根节点、左子树、右子树---]、中序遍历[---输出顺序是左子树、根节点、右子树---]、后序遍历[---输出顺序是左子树、右子树、根节点---])
- 广度优先遍历(层序遍历[---层序遍历,顾名思义,就是二叉树按照从根节点到叶子节点的层次关系,一层一层横向遍历各个节点。---])
树实现的常用集合
TreeMap、TreeSet
堆(Heap)
堆(英语:Heap)是计算机科学中的一种特别的完全二叉树。若是满足以下特性,即可称为堆:“给定堆中任意节点P和C,若P是C的母节点,那么P的值会小于等于(或大于等于)C的值”。若母节点的值恒小于等于子节点的值,此堆称为最小堆(min heap);反之,若母节点的值恒大于等于子节点的值,此堆称为最大堆(max heap)。在堆中最顶端的那一个节点,称作根节点(root node),根节点本身没有母节点(parent node)。
堆的优缺点
优点:插入,删除快,对最大数据的项存取很快
缺点:对其他数据项存取很慢
堆的实际应用
二叉堆是实现堆排序及优先队列的基础,注意一些优先队列在项目中的实际应用
堆实现的常用集合
暂无
图(Graph)
图(英语:graph)是一种抽象数据类型,用于实现数学中图论的无向图和有向图的概念。
图的数据结构包含一个有限(可能是可变的)的集合作为节点集合,以及一个无序对(对应无向图)或有序对(对应有向图)的集合作为边(有向图中也称作弧)的集合。节点可以是图结构的一部分,也可以是用整数下标或引用表示的外部实体。
图的数据结构还可能包含和每条边相关联的数值(edge value),例如一个标号或一个数值(即权重,weight;表示花费、容量、长度等)。
图的优缺点
优点:对现实世界建模
缺点:有些算法慢且复杂
图的常见数据结构
邻接表:节点存储为记录或对象,且为每个节点创建一个列表。这些列表可以按节点存储其余的信息;例如,若每条边也是一个对象,则将边存储到边起点的列表上,并将边的终点存储在边这个的对象本身。
邻接矩阵:一个二维矩阵,其中行与列分别表示边的起点和终点。顶点上的值存储在外部。矩阵中可以存储边的值。
关联矩阵:一个二维矩阵,行表示顶点,列表示边。矩阵中的数值用于标识顶点和边的关系(是起点、是终点、不在这条边上等)
常用集合源码解析
常用集合基本上都是围绕着数据结构的去实现的(源码层面解析),必须掌握的ArrayList、LinkedList、HashMap
ArrayList、LinkedList、Vector、HashSet、LinkedHashSet、TreeSet、HashMap、HashTable、TreeMap、LinkedHashMap AbstractQueue、BlockingQueue、Deque最好都学习一些