数据结构的整体介绍
1.什么是数据、结构?
1.1数据
数据:信息的表现形式
1.2结构
结构:由多个相关联的元素组成的整体或组织形式、即一种特定的数据组织形式,用于存储和操作数据。
2.数据结构组成:
2.1逻辑关系
逻辑关系分为:线性数据结构,是一种数据元素之间存在一对一关系的数据结构、数据元素之间存在一个明确的先后关系,每个 元素都有唯一的前驱和后继,【数组、链表、栈、队列、哈希表】;非线性数据结构【树、图、堆】。
2.1.1线性数据结构
线性数据结构是一种数据元素之间存在一对一关系的数据结构。在线性数据结构中,数据元素之间存在一个明确的先后关系, 每个元素都有唯一的前驱和后继。常见的线性数据结构包括:
2.1.1.1数组
数组:顺序存储、连续
//数组定义,for遍历
int[]array = new int[]{10, 20, 30, 40, 50};
for(int i = 0; i < array.length; i++){
System.out.println(array[i]);
}
//数组定义,foreach(加强for)循环遍历
int[] array = {1, 2, 3};
for (int x : array) {
System.out.println(x);
}
2.1.1.2链表
链表【单向、双向】:单向链表:每个节点只有一个指针,指向下一个节点。最后一个节点的指针指向空。双向链表:每个 节点有两个指针,一个指向前一个节点,一个指向后一个节点。首尾节点的指针分别指向空。顺序、链式存储
public class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
this.next = null;
}
}
public class LinkedList {
ListNode head;
public LinkedList() {
this.head = null;
}
public void add(int val) {
ListNode newNode = new ListNode(val);
if (head == null) {
head = newNode;
} else {
ListNode current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
public void display() {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.display();
}
}
这个示例中,我们定义了一个ListNode类来表示链表的节点,它包含一个整数值val和一个指向下一个节点的指针next。然后,我们定义了一个LinkedList类来表示链表,它有一个头节点head。我们可以通过add方法向链表中添加新的节点,display方法用于打印链表中的所有节点。
2.1.1.3栈
栈:栈底、栈顶(FILO先进[入栈]后出[出栈])
import java.util.EmptyStackException;
public class Stack {
private int[] array;
private int top;
private int capacity;
public Stack(int capacity) {
this.array = new int[capacity];
this.top = -1;
this.capacity = capacity;
}
public boolean isEmpty() {
return top == -1;
}
public boolean isFull() {
return top == capacity - 1;
}
public void push(int item) {
if (isFull()) {
throw new StackOverflowError("Stack is full");
}
array[++top] = item;
}
public int pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return array[top--];
}
public int peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return array[top];
}
public static void main(String[] args) {
Stack stack = new Stack(5);
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.pop()); // Output: 3
System.out.println(stack.peek()); // Output: 2
System.out.println(stack.isEmpty()); // Output: false
}
}
这个示例中,我们定义了一个Stack类来表示栈,它使用一个数组来存储栈中的元素。栈的容量由构造函数中的参数capacity指定。栈的顶部由变量top表示,初始值为-1,表示栈为空。
我们提供了以下几个方法:
isEmpty():检查栈是否为空。
isFull():检查栈是否已满。
push(int item):将元素入栈。
pop():将栈顶元素出栈并返回。
peek():返回栈顶元素,但不出栈。
在main方法中,我们创建了一个容量为5的栈对象,并向栈中压入了3个元素。然后我们进行了一些栈操作,包括弹出栈顶元素、查看栈顶元素和检查栈是否为空。最后,我们打印了一些结果。
2.1.1.4队列
队列:FIFO先进先出[入队出队]
import java.util.NoSuchElementException;
public class Queue {
private int[] array;
private int front;
private int rear;
private int capacity;
private int size;
public Queue(int capacity) {
this.array = new int[capacity];
this.front = 0;
this.rear = -1;
this.capacity = capacity;
this.size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
public void enqueue(int item) {
if (isFull()) {
throw new IllegalStateException("Queue is full");
}
rear = (rear + 1) % capacity;
array[rear] = item;
size++;
}
public int dequeue() {
if (isEmpty()) {
throw new NoSuchElementException("Queue is empty");
}
int item = array[front];
front = (front + 1) % capacity;
size--;
return item;
}
public int peek() {
if (isEmpty()) {
throw new NoSuchElementException("Queue is empty");
}
return array[front];
}
public static void main(String[] args) {
Queue queue = new Queue(5);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println(queue.dequeue()); // Output: 1
System.out.println(queue.peek()); // Output: 2
System.out.println(queue.isEmpty()); // Output: false
}
}
这个示例中,我们定义了一个Queue
类来表示队列,它使用一个数组来存储队列中的元素。队列的容量由构造函数中的参数capacity
指定。队列的前端由变量front
表示,初始值为0,表示队列为空。队列的后端由变量rear
表示,初始值为-1。
我们提供了以下几个方法:
isEmpty()
:检查队列是否为空。isFull()
:检查队列是否已满。enqueue(int item)
:将元素入队。dequeue()
:将队列头部元素出队并返回。peek()
:返回队列头部元素,但不出队。
在main
方法中,我们创建了一个容量为5的队列对象,并向队列中入队了3个元素。然后我们进行了一些队列操作,包括出队头部元素、查看头部元素和检查队列是否为空。最后,我们打印了一些结果。
2.1.2非线性数据结构
非线性数据结构是指数据元素之间不是一对一的线性关系,而是多对多、多对一或一对多的关系。
常见的非线性数据结构包括图(Graph)、树(Tree)和堆(Heap)等。
2.1.2.1树
树:具有层次结构,每个节点可以有零个或多个子节点,而一个节点只有一个父节点(除了根节点)。具有层级,二叉树、 平衡树、绝对平衡树、B+.
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public class TreeExample {
public static void main(String[] args) {
// 创建树的根节点
TreeNode root = new TreeNode(1);
// 创建左子节点
TreeNode leftNode = new TreeNode(2);
root.left = leftNode;
// 创建右子节点
TreeNode rightNode = new TreeNode(3);
root.right = rightNode;
// 创建左子节点的左子节点
TreeNode leftLeftNode = new TreeNode(4);
leftNode.left = leftLeftNode;
// 创建左子节点的右子节点
TreeNode leftRightNode = new TreeNode(5);
leftNode.right = leftRightNode;
// 遍历树
System.out.println("前序遍历:");
preOrderTraversal(root);
System.out.println("\n中序遍历:");
inOrderTraversal(root);
System.out.println("\n后序遍历:");
postOrderTraversal(root);
}
// 前序遍历
public static void preOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " ");
preOrderTraversal(root.left);
preOrderTraversal(root.right);
}
// 中序遍历
public static void inOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
inOrderTraversal(root.left);
System.out.print(root.val + " ");
inOrderTraversal(root.right);
}
// 后序遍历
public static void postOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
postOrderTraversal(root.left);
postOrderTraversal(root.right);
System.out.print(root.val + " ");
}
}
这个代码示例创建了一个简单的二叉树,并实现了前序遍历、中序遍历和后序遍历的方法。在main方法中,首先创建了树的根节点,然后创建了其他节点并将它们连接到根节点的左右子节点上。最后,调用遍历方法对树进行遍历,并打印遍历结果。
2.1.2.2图
图:由节点(Vertex)和边(Edge)组成。图的特点是节点之间可以有任意连接关系,边可以是有向的或无向的。有向 图、无向图、加权图、有向无环图等
import java.util.*;
class Graph {
private int V; // 图的顶点数
private LinkedList<Integer>[] adjList; // 邻接表
public Graph(int v) {
V = v;
adjList = new LinkedList[V];
for (int i = 0; i < V; i++) {
adjList[i] = new LinkedList<>();
}
}
public void addEdge(int src, int dest) {
adjList[src].add(dest); // 添加边
adjList[dest].add(src); // 无向图需要添加反向边
}
public void bfs(int start) {
boolean[] visited = new boolean[V]; // 记录节点是否被访问过
Queue<Integer> queue = new LinkedList<>(); // 用于广度优先搜索的队列
visited[start] = true;
queue.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
System.out.print(node + " ");
for (int neighbor : adjList[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.add(neighbor);
}
}
}
}
public void dfs(int start) {
boolean[] visited = new boolean[V]; // 记录节点是否被访问过
dfsHelper(start, visited);
}
private void dfsHelper(int node, boolean[] visited) {
visited[node] = true;
System.out.print(node + " ");
for (int neighbor : adjList[node]) {
if (!visited[neighbor]) {
dfsHelper(neighbor, visited);
}
}
}
}
public class GraphExample {
public static void main(String[] args) {
Graph graph = new Graph(5);
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 4);
System.out.println("BFS traversal:");
graph.bfs(0);
System.out.println("\nDFS traversal:");
graph.dfs(0);
}
}
这个代码示例创建了一个简单的无向图,并实现了广度优先搜索(BFS)和深度优先搜索(DFS)的方法。在Graph类中,使用邻接表来表示图的结构,使用addEdge方法添加边。在bfs方法中,使用队列来实现广度优先搜索,并使用visited数组来记录节点是否被访问过。在dfs方法中,使用递归的方式实现深度优先搜索。在main方法中,创建了一个图并添加了边,然后调用bfs和dfs方法进行遍历,并打印遍历结果。
2.2存储方式
存储方式分为顺序存储、链式存储
2.2.1顺序存储
在顺序存储中,数据元素在内存中按照一定的顺序依次存储,相邻元素在内存中也是相邻的。
顺序存储通常使用数组(Array)来实现。数组在内存中是连续存储的,每个元素占用相等大小的内存空间。
-
优点:
-
快速随机访问:可以通过索引快速访问数组中的任何元素,具有常数时间复杂度的访问。
-
内存利用高:不需要额外的指针存储元素之间的关系,因此内存利用率高。
-
-
缺点:
- 大小固定:数组的大小在创建时固定,不易动态扩展。
- 插入和删除困难:插入和删除元素可能需要移动大量元素。
2.2.2链式存储
在链式存储中,数据元素通过节点(Node)相互连接,每个节点包含数据和指向下一个节点的指针(或引用),链式存储通常使用链表(Linked List)来实现,链表节点可以在内存中不连续地存储。
- 优点:
- 动态大小:链表的大小可以动态增长或减小,更灵活。
- 插入和删除简便:插入和删除元素只需调整节点的指针,不需要移动大量元素。
- 缺点:
- 随机访问慢:访问链表中的元素需要从头节点开始遍历,具有线性时间复杂度。
- 额外的空间开销:链表节点需要额外的指针来存储连接信息,可能导致一定的空间开销。
2.3数据计算
在数据结构中,数据计算是指对存储在数据结构中的数据进行各种操作和计算的过程。数据计算可以包括以下几个方面:
- 数据插入(Insertion):向数据结构中添加新的数据元素。
- 数据删除(Deletion):从数据结构中移除指定的数据元素。
- 数据访问(Access):通过指定的索引或键值访问数据结构中的数据元素。
- 数据搜索(Search):在数据结构中查找指定的数据元素。
- 数据排序(Sorting):对数据结构中的数据元素进行排序,使其按照一定的顺序排列。
- 数据过滤(Filtering):根据指定的条件筛选数据结构中的数据元素。
- 数据统计(Statistics):对数据结构中的数据元素进行统计分析,如计算总和、平均值、最大值、最小值等。
- 数据修改(Modification):对数据结构中的数据元素进行修改,如更新、替换等。
不同的数据结构适用于不同的数据计算操作。
2.3.1算法分析
算法分析是指对算法的性能进行评估和分析的过程。在算法分析中,我们主要关注算法的时间复杂度和空间复杂度。
- 时间复杂度:时间复杂度衡量了算法执行所需的时间,通常用大O符号(O)表示。时间复杂度描述了算法执行时间随输入规模增长的增长率。常见的时间复杂度有:O(1)(常数时间复杂度)、O(log n)(对数时间复杂度)、O(n)(线性时间复杂度)、O(n log n)(线性对数时间复杂度)、O(n^2)(平方时间复杂度)等。时间复杂度越低,算法执行速度越快。
- 空间复杂度:空间复杂度衡量了算法执行所需的额外空间,通常也用大O符号(O)表示。空间复杂度描述了算法所需的额外空间随输入规模增长的增长率。常见的空间复杂度有:O(1)(常数空间复杂度)、O(n)(线性空间复杂度)、O(n^2)(平方空间复杂度)等。空间复杂度越低,算法所需的额外空间越少。
算法分析的目的是为了选择和设计高效的算法,以解决问题时能够在合理的时间和空间范围内完成。在算法分析中,我们可以使用数学推导、递归关系、实验测试等方法来评估和分析算法的性能。
2.3.2各数据结构复杂度分析
不同的数据结构在不同的操作上具有不同的时间和空间复杂度。以下是一些常见数据结构的时间和空间复杂度:
- 数组:
- 访问元素:O(1)
- 插入/删除元素:
- 在末尾插入/删除:O(1)
- 在开头插入/删除:O(n)
- 在中间插入/删除:O(n)
- 查找元素:O(n)
- 空间复杂度:O(n)
- 链表:
- 访问元素:
- 头部访问:O(1)
- 尾部访问:O(n)
- 中间访问:O(n)
- 插入/删除元素:
- 头部插入/删除:O(1)
- 尾部插入/删除:O(1)(如果有尾指针)
- 中间插入/删除:O(n)
- 查找元素:O(n)
- 空间复杂度:O(n)
- 访问元素:
- 栈:
- 入栈:O(1)
- 出栈:O(1)
- 查看栈顶元素:O(1)
- 空间复杂度:O(n)
- 队列:
- 入队:O(1)
- 出队:O(1)
- 查看队首元素:O(1)
- 空间复杂度:O(n)
- 哈希表:
- 插入/删除/查找元素:平均情况下为O(1),最坏情况下为O(n)
- 空间复杂度:O(n)
- 二叉树:
- 插入/删除/查找元素:平均情况下为O(log n),最坏情况下为O(n)
- 空间复杂度:O(n)
- 堆:
- 插入元素:O(log n)
- 删除堆顶元素:O(log n)
- 查找堆顶元素:O(1)
- 空间复杂度:O(n)
- 图:
- 遍历图:O(V + E),其中V为顶点数,E为边数
- 查找顶点/边:O(1)(如果使用邻接矩阵),O(V)(如果使用邻接表)
- 空间复杂度:O(V + E)
需要注意的是,以上时间和空间复杂度只是一般情况下的估计,在特定情况下可能会有所变化。此外,某些操作的复杂度可能会受到具体实现方式的影响。因此,在具体使用时,需要根据具体情况进行评估和选择。