1 什么是数据结构与算法
数据结构是一种组织和存储数据的方式,其目的在于使数据能够被高效地访问和修改。常见的数据结构包括数组、链表、栈、队列、树和图等。
算法则是为了实现特定目标而依附于数据结构的一系列运算过程。这些运算的目标通常是在解决问题时以最少的资源(例如时间和空间复杂度)达成预期结果。典型的算法涵盖排序算法(如快速排序、归并排序)、搜索算法(如二分查找)以及图算法(如深度优先搜索、广度优先搜索)等。二 基本的数据结构
2 数据结构
线性数据结构是一种数据组织方式,其中元素之间存在顺序关系,即每个元素都有唯一的前驱和后继,除了第一个元素没有前驱,最后一个元素没有后继。这样的结构形成一条线(线性结构),元素的排列是有序的。
线性数据结构有:数组(Array)链表(Linked List)栈(Stack)队列(Queue)等等
2.1 数组(Array)
介绍:
数组是一种线性数据结构,它由一系列相同类型的元素组成,这些元素在内存中是连续存储的。每个元素都有一个唯一的索引,通过索引可以直接访问数组中的元素。
能力:
1 存储一系列相同类型的数据。
2 提供快速随机访问元素的能力。
3 在一维数组的基础上,可以构建多维数组,如二维数组、三维数组等。
基本操作时间复杂度:
- 访问元素: O(1) - 数组通过索引直接访问元素,时间复杂度是常数级别。
- 插入元素: 平均情况下为 O(n) - 需要移动插入位置后的所有元素。
- 删除元素: 平均情况下为 O(n) - 需要移动删除位置后的所有元素。
- 更新元素: O(1) - 通过索引直接修改元素。
数组的优点:
-
随机访问: 通过索引可以在 O(1) 的时间复杂度内直接访问数组中的元素,使得随机访问非常高效。
-
内存连续性: 数组的元素在内存中是连续存储的,这有助于缓存性能的提升。
-
简单易用: 数组是一种简单而直观的数据结构,容易理解和使用。
数组的缺点:
-
固定大小: 数组在创建时需要指定大小,且大小是固定的。如果需要存储的元素数量不确定,可能需要重新创建数组,导致内存浪费或者需要动态调整数组大小,增加了复杂性。
-
插入和删除效率低: 在数组中插入或删除元素通常需要移动其他元素,时间复杂度为 O(n),其中 n 是数组的长度。
-
空间浪费: 如果数组的大小远远大于实际存储的元素数量,会导致空间浪费。
数组的应用场景:
-
随机访问需求: 当需要在常量时间内通过索引直接访问元素时,数组是一个理想的选择。
-
固定大小的数据集: 当数据集的大小是固定的,并且需要高效的随机访问时,数组是一种合适的数据结构。
-
元素的插入和删除操作较少: 如果程序中对元素的插入和删除操作相对较少,而更多地是访问和遍历操作,数组是一个合适的选择。
-
多维数据集: 数组天生支持多维结构,适用于表示矩阵、图像和其他多维数据。
需要注意的是,对于频繁插入、删除、动态变化的数据集,可能需要考虑使用其他数据结构,比如链表或动态数组(ArrayList)等。不同的数据结构适用于不同的应用场景。
数组代码
public class Starts {
public static void main(String[] args) {
//新建一个数组
TestArr testArr = new TestArr(10);
testArr.add(1);
testArr.add(2);
testArr.add(3);
testArr.add(4);
testArr.add(5);
testArr.printArr();
testArr.interposition(2,7);
testArr.printArr();
testArr.del(2);
testArr.printArr();
System.out.println(testArr.find(4));
}
}
public class TestArr {
int[] arr = null;
int size = 0; // 将 size 初始化为0
public TestArr(int size) {
this.arr = new int[size];
}
//添加 尾追加
public void add(int value) {
// 判断是否满了
if (size >= arr.length) {
System.out.println("满了!");
return;
}
arr[size] = value;
size++;
}
//直接更新数组对应下标
public void up(int subscript, int value) {
// 判断下标是否越界
if (subscript >= arr.length) {
System.out.println("超长!");
return;
}
arr[subscript] = value;
}
// 插入
public void interposition(int subscript, int value) {
// 判断下标是否越界
if (subscript > arr.length || subscript < 0) {
System.out.println("超长!");
return;
}
for (int i = size; i >= subscript; i--) {
arr[i + 1] = arr[i];
}
arr[subscript] = value;
size++;
}
// 删除
public void del(int subscript) {
// 判断下标是否越界
if (subscript >= arr.length || subscript < 0) {
System.out.println("超长!");
return;
}
for (int i = subscript; i < size; i++) {
arr[i] = arr[i + 1];
}
size--;
}
// 查找 返回对应数据的下标
public boolean find(int num) {
for (int i = 0; i <= size; i++) {
if (arr[i] == num) {
return true;
}
}
return false;
}
public void printArr() {
for (int i = 0; i <= size; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
2.2 链表(Linked List)
什么是链表:
链表是一种线性数据结构,由节点(Node)组成,每个节点包含数据和指向下一个节点的指针(或引用)。与数组不同,链表的元素在内存中可以是非连续存储的,通过节点之间的指针连接。
能力:
- 动态大小: 链表可以根据需要动态地分配内存,支持灵活的大小调整。
- 插入和删除高效: 插入和删除节点的操作在链表中是高效的,因为只需要调整相邻节点的指针,而不涉及元素的移动。
- 无需预先分配内存: 与数组不同,链表无需预先分配固定大小的内存空间。
操作:
-
插入节点(Insertion):
- 时间复杂度:O(n)
- 在最坏情况下,可能需要遍历整个链表来找到插入位置。
- 空间复杂度:O(1)
- 仅需额外的空间存储新节点。
- 时间复杂度:O(n)
-
删除节点(Deletion):
- 时间复杂度:O(n)
- 在最坏情况下,可能需要遍历整个链表来找到要删除的节点。
- 空间复杂度:O(1)
- 仅需额外的空间存储指针。
- 时间复杂度:O(n)
-
查找节点(Search):
- 时间复杂度:O(n)
- 在最坏情况下,可能需要遍历整个链表来找到目标节点。
- 空间复杂度:O(1)
- 仅需额外的空间存储指针。
- 时间复杂度:O(n)
-
遍历链表(Traversal):
- 时间复杂度:O(n)
- 需要遍历整个链表,其中 n 是链表的长度。
- 空间复杂度:O(1)
- 仅需额外的空间存储指针。
- 时间复杂度:O(n)
-
获取链表长度:
- 时间复杂度:O(n)
- 在遍历链表的过程中计算节点数量。
- 空间复杂度:O(1)
- 仅需额外的空间存储计数器。
- 时间复杂度:O(n)
总体而言,单向链表的基本操作中,时间复杂度主要受到遍历的影响,因为大多数操作都需要遍历链表来定位节点。空间复杂度相对较低,仅需额外的空间存储指针或计数器。在特定的应用场景中,链表的优势主要体现在插入和删除操作上,尤其是在频繁执行这些操作时。
优点:
- 动态大小: 链表支持动态大小,可以根据需要轻松调整。
- 高效的插入和删除: 插入和删除节点操作在链表中是高效的,不需要移动大量元素。
- 无需预先分配内存: 链表不需要预先分配固定大小的内存空间,因此不会浪费内存。
缺点:
- 随机访问效率低: 在链表中,要访问特定位置的元素,需要从头节点开始遍历,因此随机访问效率较低。
- 额外的存储空间: 每个节点需要额外的存储空间来存储指针,增加了存储开销。
- 缓存不友好: 由于节点在内存中不是连续存储的,可能导致缓存不友好,降低访问效率。
链表类型:
单向链表(Singly Linked List)
适用于简单的插入和删除操作,对于需要快速访问前一个节点的情况较少。
单向循环链表(Singly Circular Linked List)
当需要循环遍历链表,而且对于结束条件不敏感时。
双向链表(Doubly Linked List)
在插入和删除操作频繁、需要快速找到前一个节点的情况下使用。
双向循环链表(Doubly Circular Linked List)
当需要循环遍历链表,并且对于结束条件不敏感,同时有频繁的插入和删除操作时
在选择链表类型时,需要根据实际需求考虑操作的频率、对内存占用的要求以及对访问速度的要求。不同的链表类型在不同场景下有各自的优势和劣势。
单向链表操作代码:
public class Node {
//指向后部节点
public Node next;
//指向后部节点
public Node pre;
int val;
public Node(int val) {
this.val = val;
}
}
public class UnidirectionalLinked {
Node head;
//尾部追加节点
public void add(Node node) {
if (head == null) {
head = node;
return;
}
Node boxNode = head;
while (boxNode.next != null) {
boxNode = boxNode.next;
}
boxNode.next = node;
}
//中间追加节点
public void add(int nodeValue, Node node) {
Node node1 = findNodeByValue(nodeValue);
if (node1 == null) {
return;
}
Node next = node1.next;
node1.next = node;
node.next = next;
}
//头结点插入
public void addHead(Node node) {
node.next = head;
head = node;
}
//找到对应值的节点
public Node findNodeByValue(int value) {
Node boxNode = head;
while (boxNode != null) {
if (boxNode.val == value) {
return boxNode;
}
boxNode = boxNode.next;
}
return boxNode;
}
//中间删除指定节点
public void delNode(int value){
Node boxNode = head;
Node pre = null;
while (boxNode != null) {
if (boxNode.val == value) {
break;
}
pre = boxNode;
boxNode = boxNode.next;
}
if(pre==null){
head = boxNode.next;
}else{
pre.next = boxNode.next;
boxNode.next = null;
}
}
//循环打印每一个节点
public void print( ){
Node boxNode = head;
while (boxNode != null) {
System.out.print(boxNode.val + " ");
boxNode = boxNode.next;
}
System.out.println();
}
}
public class Starts {
public static void main(String[] args) {
//创建一个链表
UnidirectionalLinked linked = new UnidirectionalLinked();
linked.add(new Node(1));
linked.add(new Node(2));
linked.add(new Node(3));
linked.add(new Node(4));
linked.print();
//在链表中插入
linked.add(1,new Node(9));
linked.print();
//在链表中头结点插入
linked.addHead(new Node(0));
linked.print();
//在链表中删除
linked.delNode(4);
linked.print();
//在链表中删除
linked.delNode(0);
linked.print();
}
}
2.3 树-二叉树(Binary Tree)
什么是二叉树
- 定义: 二叉树是一种树形结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。
- 特性:
- 每个节点最多有两个子节点,分别称为左子节点和右子节点。
- 左子树和右子树都是二叉树。
- 二叉树的子节点顺序是有序的,即左子节点在前,右子节点在后。
一般用来做什么:
- 常见应用:
- 数据搜索:二叉搜索树(BST)是一种特殊的二叉树,支持高效的查找、插入和删除操作。
- 表达式树:用于表示和计算表达式,例如算术表达式。
- 文件系统:文件系统中的目录结构可以被表示为一棵树。
- 编码与解码:Huffman树用于数据压缩。
优点:
- 高效的搜索: 在二叉搜索树中,搜索操作的平均时间复杂度为O(log n),使得数据检索非常高效。
- 易于实现: 相对于其他树结构,二叉树的实现相对简单。
缺点:
- 可能退化为链表: 如果插入的数据是有序的,二叉树可能会退化为链表,导致搜索性能下降。
- 不适用于特殊需求: 有些应用场景可能需要更复杂的树结构,例如平衡树、B树等。
二叉树的基本操作
1. 插入节点:
操作:
-
在二叉树中插入新节点。
时间复杂度:
-
平均情况下为 O(log n),其中 n 是二叉树的节点数。
-
最坏情况下可能退化为链表,时间复杂度为 O(n)。
空间复杂度:
-
O(1),仅需要额外的空间存储新节点。
2. 删除节点:
操作:
-
从二叉树中删除指定节点。
时间复杂度:
-
平均情况下为 O(log n),其中 n 是二叉树的节点数。
-
最坏情况下可能退化为链表,时间复杂度为 O(n)。
空间复杂度:
-
O(1),仅需要额外的空间存储指针
3. 查找节点:
操作:
-
在二叉树中查找具有特定值的节点。
时间复杂度:
-
平均情况下为 O(log n),其中 n 是二叉树的节点数。
-
最坏情况下可能退化为链表,时间复杂度为 O(n)。
空间复杂度:
-
O(1),仅需要额外的空间存储指针。
4. 遍历二叉树:
操作:
-
按照特定顺序访问二叉树中的所有节点。
时间复杂度:
-
遍历整个二叉树的时间复杂度为 O(n),其中 n 是二叉树的节点数。
空间复杂度:
-
O(h),其中 h 是二叉树的高度。递归实现的遍历操作可能会使用到系统栈,其空间复杂度与二叉树的高度相关。
5. 获取二叉树的高度:
操作:
-
计算二叉树的高度,即从根节点到叶子节点的最长路径。
时间复杂度:
-
O(n),其中 n 是二叉树的节点数。
空间复杂度:
-
O(h),其中 h 是二叉树的高度。递归实现的操作可能使用到系统栈,其空间复杂度与二叉树的高度相关。
总体而言,二叉树的基本操作的时间复杂度主要取决于二叉树的结构。在平衡的二叉搜索树(如AVL树)中,这些操作的平均时间复杂度通常为 O(log n),但在不平衡的情况下,可能达到 O(n)。递归实现的操作可能会使用到系统栈,因此其空间复杂度与二叉树的高度相关。
其他树类型的数据结构有哪些:
-
平衡二叉树(Balanced Binary Tree): 保持树的平衡性,使得每个节点的左右子树高度差不超过1,例如AVL树。
-
B树(B-Tree): 用于处理大量数据的存储和搜索,数据库中常用于索引。
-
红黑树(Red-Black Tree): 一种自平衡的二叉搜索树,常用于实现集合、映射等数据结构。
-
Trie树(Trie Tree): 用于高效存储和检索大量的字符串,常用于字典、拼写检查等应用。
-
堆(Heap): 用于实现优先队列等,分为最大堆和最小堆。
-
树堆(Treap): 结合了树和堆的特性,用于高效的插入和删除操作。
-
树状数组(Binary Indexed Tree,Fenwick Tree): 用于高效计算数组的前缀和,主要用于范围查询。
每种树结构都有其特定的应用场景和优劣势,选择合适的树结构取决于问题的性质和需求。
二叉树遍历
二叉树的遍历分为前序遍历、中序遍历和后序遍历,它们是指遍历树中节点的顺序。以下是它们的具体遍历方法以及一些使用场景:
1. 前序遍历(Preorder Traversal):
遍历方法:
-
访问根节点。
-
递归地前序遍历左子树。
-
递归地前序遍历右子树。
使用场景:
-
需要优先处理根节点的操作。
-
用于复制一棵树。
2. 中序遍历(Inorder Traversal):
遍历方法:
-
递归地中序遍历左子树。
-
访问根节点。
-
递归地中序遍历右子树。
使用场景:
-
需要按照节点值的大小顺序处理节点的操作。
-
用于二叉搜索树的中序遍历可以得到有序序列。
3. 后序遍历(Postorder Traversal):
遍历方法:
-
递归地后序遍历左子树。
-
递归地后序遍历右子树。
-
访问根节点。
使用场景:
-
需要优先处理叶子节点的操作。
-
用于内存释放,在删除整棵树时先删除子树。
二叉树代码(前序中序后序)
public class TreeNode {
public TreeNode left;
public TreeNode right;
int value;
public TreeNode(int value) {
this.value = value;
}
//前序 中左右
public void preorder(TreeNode head){
if(head==null){
return;
}
System.out.print(head.value+" ");
preorder(head.left);
preorder(head.right);
}
//中序 左中右
public void infixOrder(TreeNode head){
if(head==null){
return;
}
infixOrder(head.left);
System.out.print(head.value+" ");
infixOrder(head.right);
}
//后序列 左右中
public void followUp(TreeNode head){
if(head==null){
return;
}
followUp(head.left);
followUp(head.right);
System.out.print(head.value+" ");
}
// 计算二叉树的高度
public int height(TreeNode root) {
if (root == null) {
return 0;
} else {
int leftHeight = height(root.left);
int rightHeight = height(root.right);
// 返回左右子树高度的较大值加1
return Math.max(leftHeight, rightHeight) + 1;
}
}
// 新增方法:生成指定高度的随机二叉树
public static TreeNode generateRandomBinaryTree(int height) {
Random random = new Random();
return generateRandomBinaryTreeHelper(height, random);
}
private static TreeNode generateRandomBinaryTreeHelper(int height, Random random) {
if (height == 0 || random.nextInt(5) == 0) {
// 有 1/5 的概率结束树的生长,或者已经达到指定高度
return null;
}
TreeNode node = new TreeNode(random.nextInt(100) + 1);
node.left = generateRandomBinaryTreeHelper(height - 1, random);
node.right = generateRandomBinaryTreeHelper(height - 1, random);
return node;
}
}
public class Starts {
public static void main(String[] args) {
//构造一棵树
TreeNode t1 = TreeNode.generateRandomBinaryTree(5);
//前序
t1.preorder(t1);
System.out.println();
//中序
t1.infixOrder(t1);
System.out.println();
//后续
t1.followUp(t1);
System.out.println();
}
}
2.4 栈
什么是栈:
栈是一种具有特定操作规则的线性数据结构,遵循先进后出(Last In, First Out,LIFO)的原则。在栈中,最后插入的元素是最先被访问和删除的。
基本操作:
- Push: 将元素压入栈顶。
- Pop: 从栈顶弹出元素。
- Peek/Top: 查看栈顶元素,但不弹出。
- isEmpty: 判断栈是否为空。
- Size: 获取栈中元素个数。
使用场景:
- 函数调用: 函数的调用和返回过程中,使用栈来保存函数调用信息。
- 表达式求值: 中缀表达式转后缀表达式,以及后缀表达式的求值,可以使用栈。
- 浏览器前进后退: 浏览器的前进和后退功能可以使用两个栈来实现。
- 括号匹配: 判断括号是否匹配可以借助栈的特性。
- Undo/Redo 功能: 许多编辑器和软件中的撤销和重做功能使用栈来管理操作历史。
优点:
- 简单高效: 栈的基本操作时间复杂度为 O(1)。
- 空间效率: 栈在空间上比其他数据结构(如链表)更节省空间。
缺点:
- 容量限制: 栈的容量在开始时固定,如果存储元素超过容量,可能导致栈溢出。
- 不适合随机访问: 栈只能通过顺序访问元素,不支持随机访问。
总体而言,栈是一种简单而有效的数据结构,特别适用于涉及到后进先出的场景。在编程和算法中,栈的应用非常广泛。
代码
数组实现
public class ArrayStack {
private int maxSize; // 栈的最大容量
private int[] stackArray; // 用数组实现栈
private int top; // 栈顶指针
// 构造方法,初始化栈
public ArrayStack(int size) {
this.maxSize = size;
this.stackArray = new int[maxSize];
this.top = -1; // 初始时栈为空,栈顶指针为-1
}
// 入栈操作
public void push(int value) {
if (top < maxSize - 1) {
stackArray[++top] = value;
} else {
System.out.println("栈溢出");
}
}
// 出栈操作
public int pop() {
if (top >= 0) {
return stackArray[top--];
} else {
System.out.println("栈为空");
return -1; // 表示栈为空或者栈已经清空
}
}
// 查看栈顶元素
public int peek() {
if (top >= 0) {
return stackArray[top];
} else {
System.out.println("栈为空");
return -1; // 表示栈为空
}
}
// 判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
// 获取栈的大小
public int size() {
return top + 1;
}
// 主函数测试
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(5);
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("栈顶元素: " + stack.peek());
System.out.println("弹出元素: " + stack.pop());
System.out.println("栈大小: " + stack.size());
System.out.println("栈是否为空? " + stack.isEmpty());
}
}
链表实现
class Node {
int data; // 节点数据
Node next; // 指向下一个节点的引用
// 构造方法,初始化节点
public Node(int data) {
this.data = data;
this.next = null;
}
}
public class LinkedStack {
private Node top; // 栈顶节点
// 构造方法,初始化栈
public LinkedStack() {
this.top = null; // 初始时栈为空
}
// 入栈操作
public void push(int value) {
Node newNode = new Node(value);
newNode.next = top;
top = newNode;
}
// 出栈操作
public int pop() {
if (top != null) {
int value = top.data;
top = top.next;
return value;
} else {
System.out.println("栈为空");
return -1; // 表示栈为空或者栈已经清空
}
}
// 查看栈顶元素
public int peek() {
if (top != null) {
return top.data;
} else {
System.out.println("栈为空");
return -1; // 表示栈为空
}
}
// 判断栈是否为空
public boolean isEmpty() {
return top == null;
}
// 获取栈的大小
public int size() {
int count = 0;
Node current = top;
while (current != null) {
count++;
current = current.next;
}
return count;
}
// 主函数测试
public static void main(String[] args) {
LinkedStack stack = new LinkedStack();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("栈顶元素: " + stack.peek());
System.out.println("弹出元素: " + stack.pop());
System.out.println("栈大小: " + stack.size());
System.out.println("栈是否为空? " + stack.isEmpty());
}
}
2.5 队列
什么是队列:
队列是一种基于先进先出(First In, First Out,FIFO)原则的线性数据结构。在队列中,新元素被添加到队尾,而从队列中移除元素则发生在队头。
基本操作:
- Enqueue(入队): 将元素添加到队尾。
- Dequeue(出队): 从队头移除元素。
- Front(查看队头元素): 查看队头元素,但不移除。
- IsEmpty: 判断队列是否为空。
- Size: 获取队列中元素的个数。
使用场景:
- 任务调度: 操作系统中的进程调度,任务按照先到先服务的原则进行排队执行。
- 广度优先搜索(BFS): 图的遍历算法中,广度优先搜索可以借助队列实现。
- 打印任务: 打印机队列中,先提交的打印任务会先被执行。
- 消息传递: 系统间通信中,消息队列常用于存储和传递消息。
优点:
- 简单高效: 队列的基本操作时间复杂度为 O(1)。
- 先进先出: 适合需要按照先来后到顺序处理任务的场景。
缺点:
- 不适合随机访问: 队列只支持顺序访问,不支持随机访问。
相关数据结构:
- 双端队列(Deque): 可以在队列两端进行入队和出队操作。
- 优先队列: 元素的出队顺序依赖于元素的优先级。
队列实现方式:数组实现队列
public class ArrayQueue {
private int maxSize;//记录队列的大小
private int[] queueArray;
private int front; // 队头指针
private int rear; // 队尾指针
private int size; //队列中元素数量
public ArrayQueue(int size) {
this.maxSize = size; // 预留一个位置用于区分队空和队满
this.queueArray = new int[maxSize];
this.front = 0;
this.rear = 0;
this.size = 0;
}
// 入队操作
public void enqueue(int value) {
if (!isFull()) {
queueArray[rear] = value;
rear = (rear+1) % maxSize;
size++;
} else {
System.out.println("队列已满");
}
}
// 出队操作
public int dequeue() {
if (!isEmpty()) {
int value = queueArray[front];
front = (front+1) % maxSize;
size--;
return value;
} else {
System.out.println("队列为空");
return -1; // 表示队列为空
}
}
// 查看队头元素
public int front() {
if (!isEmpty()) {
return queueArray[front];
} else {
System.out.println("队列为空");
return -1; // 表示队列为空
}
}
// 判断队列是否为空
public boolean isEmpty() {
return size==0;
}
// 判断队列是否已满
public boolean isFull() {
return size>=maxSize;
}
// 获取队列中元素的个数
public int size() {
return size;
}
public void printQueueMsg(){
System.out.print("队头指针: "+front);
System.out.print(",队尾指针: "+rear);
System.out.println(",队列大小: "+size);
}
// 主函数测试
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(5);
queue.enqueue(1);
queue.printQueueMsg();
queue.enqueue(2);
queue.printQueueMsg();
queue.enqueue(3);
queue.printQueueMsg();
queue.enqueue(4);
queue.printQueueMsg();
queue.enqueue(5);
queue.printQueueMsg();
queue.enqueue(6);
queue.printQueueMsg();
//出队列
System.out.println("队头元素: " + queue.front());
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
queue.enqueue(6);
queue.printQueueMsg();
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
System.out.println("出队元素: " + queue.dequeue());
queue.printQueueMsg();
}
}
队列实现方式:链表实现队列
public class Node {
int data; // 节点数据
Node next; // 指向下一个节点的引用
// 构造方法,初始化节点
public Node(int data) {
this.data = data;
this.next = null;
}
}
public class LinkedQueue {
private Node front; // 队列的前端
private Node rear; // 队列的尾端
// 队列构造函数,初始化为空队列
public LinkedQueue() {
this.front = null;
this.rear = null;
}
// 检查队列是否为空
public boolean isEmpty() {
return front == null;
}
// 将元素入队
public void enqueue(int data) {
Node newNode = new Node(data);
// 如果队列为空,新节点既是前端又是尾端
if (isEmpty()) {
front = rear = newNode;
} else {
rear.next = newNode; // 将新节点链接到当前尾端的后面
rear = newNode; // 更新尾端为新节点
}
}
// 将元素出队,并返回出队的元素值
public int dequeue() {
if (isEmpty()) {
throw new IllegalStateException("Queue is empty");
}
int data = front.data; // 获取前端节点的数据
front = front.next; // 将前端指针移动到下一个节点
// 如果队列只有一个元素被出队后为空队列,需要同时更新尾端为null
if (front == null) {
rear = null;
}
return data;
}
// 查看队首元素的值,但不出队
public int peek() {
if (isEmpty()) {
throw new IllegalStateException("Queue is empty");
}
return front.data;
}
// 显示队列中的所有元素
public void display() {
Node current = front;
while (current != null) {
System.out.print(current.data + " ");
current = current.next;
}
System.out.println();
}
// 主函数用于测试队列操作
public static void main(String[] args) {
LinkedQueue queue = new LinkedQueue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println("Queue elements:");
queue.display();
System.out.println("Dequeue: " + queue.dequeue());
System.out.println("Peek: " + queue.peek());
System.out.println("Queue elements after dequeue:");
queue.display();
}
}
3 基本排序算法
排序算法是一种将一组元素按照一定的顺序重新排列的算法。排序算法通常被应用于数据处理和计算机科学的多个领域,例如搜索、数据库操作、图形处理等。排序的目的是使得数据按照升序或降序的方式有序排列,以便于快速查找和检索。
排序算法可以分为多种类型,主要包括以下几类:
-
比较排序: 根据元素之间的比较关系进行排序。
- 冒泡排序(Bubble Sort)
- 选择排序(Selection Sort)
- 插入排序(Insertion Sort)
- 归并排序(Merge Sort)
- 快速排序(Quick Sort)
- 堆排序(Heap Sort)
- 希尔排序(Shell Sort)
-
非比较排序: 不通过比较元素之间的大小关系进行排序。
- 计数排序(Counting Sort)
- 桶排序(Bucket Sort)
- 基数排序(Radix Sort)
每种排序算法都有其适用的场景和性能特点。在选择排序算法时,通常需要根据数据的规模、数据的分布情况以及对排序稳定性的要求来进行选择。
3.1选择排序(Selection Sort)
排序方式:
选择排序是一种简单直观的排序算法,其基本思想是每次从未排序的部分选取最小(或最大)的元素,然后放到已排序部分的末尾。具体步骤如下:
- 从待排序序列中找到最小(或最大)的元素。
- 将找到的最小(或最大)元素与待排序序列的第一个元素交换位置。
- 在剩余的未排序序列中重复步骤1和步骤2,直到所有元素都被排序。
时间复杂度:
- 最坏时间复杂度:O(n^2)
- 平均时间复杂度:O(n^2)
- 最好时间复杂度:O(n^2)
选择排序的时间复杂度是固定的,不受输入数据的影响,因此在数据规模较小时,选择排序可能比其他高级排序算法更具竞争力。
空间复杂度:
选择排序是一种原地排序算法,其空间复杂度为O(1),因为它仅使用了常数级别的额外空间用于存储少量变量。
稳定性:
选择排序是一种不稳定的排序算法。不稳定性的原因在于交换位置的时候可能会破坏相同元素的相对顺序。
选择排序的特点:
- 简单直观: 实现简单,不需要额外的存储空间。
- 不适用于大规模数据: 在数据规模较大时,选择排序的性能较差,因为其时间复杂度是平方级别的。
虽然选择排序不是最高效的排序算法,但由于其简单性,对于小规模数据或部分有序的数据,选择排序仍然是一种可以考虑的排序方法。
代码
public class SelectionSort {
public static void main(String[] args) {
int[] array = {64, 34, 25, 12, 22, 11, 90};
System.out.println("排序前数组:");
printArray(array);
selectionSort(array);
System.out.println("\n排序后数组:");
printArray(array);
}
// 选择排序算法
static void selectionSort(int[] arr) {
int n = arr.length;
// 遍历数组
for (int i = 0; i < n - 1; i++) {
// 找到未排序部分的最小元素的索引
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将找到的最小元素与当前元素交换
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
// 打印数组元素
static void printArray(int[] arr) {
for (int value : arr) {
System.out.print(value + " ");
}
}
}
3.2冒泡排序(Bubble Sort)
排序方式:
冒泡排序是一种简单的排序算法,其基本思想是多次遍历待排序序列,每次比较相邻两个元素,如果它们的顺序不满足要求(升序或降序),则交换它们的位置,直到整个序列有序。具体步骤如下:
- 从序列的第一个元素开始,依次比较相邻的两个元素,如果顺序不满足要求,则交换它们的位置。
- 继续对序列的下一对相邻元素进行比较和交换,直到遍历整个序列。
- 重复步骤1和步骤2,直到序列有序。
时间复杂度:
- 最坏时间复杂度:O(n^2)
- 平均时间复杂度:O(n^2)
- 最好时间复杂度:O(n)
冒泡排序的最好时间复杂度是在序列已经有序的情况下,因为每次遍历都没有发生交换,只需进行一次遍历即可。
空间复杂度:
冒泡排序是一种原地排序算法,其空间复杂度为O(1),因为它仅使用了常数级别的额外空间用于存储少量变量。
稳定性:
冒泡排序是一种稳定的排序算法。稳定性是指在排序过程中,相等元素的相对顺序不发生改变。
冒泡排序的特点:
- 简单直观: 冒泡排序是一种容易理解和实现的排序算法。
- 适用于小规模数据: 在数据规模较小时,冒泡排序的性能较好。
- 适用于部分有序数据: 对于部分有序的序列,冒泡排序的性能也相对较好。
尽管冒泡排序的时间复杂度较高,但在一些特殊情况下,它仍然是一种可行的排序方法。由于其简单性,冒泡排序也常被用于教学和理解排序算法的基本概念。
代码
public class BubbleSort {
//冒泡排序
//2 目标 让他从小到大排序
public static void main(String[] args) {
//1 有一个数组,数字顺序随机
int[] arr = new int[]{64, 34, 25, 12, 22, 11, 90};
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
printArr(arr);
}
public static void printArr(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
3.3插入排序(Insertion Sort)
排序方式:
插入排序是一种基于比较的排序算法,其基本思想是将待排序序列分成已排序和未排序两部分,初始时已排序部分只包含第一个元素,然后逐步将未排序部分的元素插入到已排序部分,直到整个序列有序。具体步骤如下:
- 将序列划分为已排序部分和未排序部分。
- 从未排序部分取出一个元素,插入到已排序部分的适当位置,使得已排序部分仍然有序。
- 重复步骤2,直到未排序部分为空。
时间复杂度:
- 最坏时间复杂度:O(n^2)
- 平均时间复杂度:O(n^2)
- 最好时间复杂度:O(n)
插入排序在最好的情况下,即序列已经有序的情况下,时间复杂度可以降至O(n),这是插入排序的优势之一。
空间复杂度:
插入排序是一种原地排序算法,其空间复杂度为O(1),因为它仅使用了常数级别的额外空间用于存储少量变量。
稳定性:
插入排序是一种稳定的排序算法。稳定性是指在排序过程中,相等元素的相对顺序不发生改变。
插入排序的特点:
- 适用于小规模数据: 插入排序在数据规模较小的情况下表现较好,尤其是在数据局部有序的情况下。
- 适用于链表: 插入排序对于链表的操作较为简便,因此在链表上的性能通常较好。
- 稳定排序: 插入排序是一种稳定的排序算法,适用于对相等元素保持相对顺序的场景。
插入排序的优势:
- 在数据规模较小的情况下,插入排序通常比其他高级排序算法性能更好。
- 插入排序是一种在线排序算法,对数据流的排序适用。
插入排序的主要缺点是其平均和最坏时间复杂度较高,不适用于大规模数据的排序。在处理大规模数据集时,通常会选择更高效的排序算法,如快速排序、归并排序等。
代码
public class InsertionSort {
public static void main(String[] args) {
int[] array = {64, 34, 25, 12, 22, 11, 90};
System.out.println("排序前数组:");
printArray(array);
insertionSort(array);
System.out.println("\n排序后数组:");
printArray(array);
}
// 插入排序算法
static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; ++i) {
int key = arr[i];
int j = i - 1;
// 将比 key 大的元素向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
// 将 key 插入到正确的位置
arr[j + 1] = key;
}
}
// 打印数组元素
static void printArray(int[] arr) {
for (int value : arr) {
System.out.print(value + " ");
}
}
}
3.4快速排序(Quick Sort)
排序方式:
快速排序是一种基于分治思想的排序算法,其基本思想是选择一个基准元素,通过一趟排序将待排序序列分割成独立的两部分,其中一部分的所有元素小于基准,另一部分的所有元素大于基准。然后对这两部分分别递归地进行快速排序。具体步骤如下:
- 选择一个基准元素,将序列分为两部分,使得左边部分的元素都小于基准,右边部分的元素都大于基准。
- 对左右两部分分别递归进行快速排序。
时间复杂度:
- 最坏时间复杂度:O(n^2)
- 平均时间复杂度:O(n log n)
- 最好时间复杂度:O(n log n)
快速排序在平均情况下具有较高的性能,是一种效率较高的排序算法。
空间复杂度:
快速排序是一种原地排序算法,其空间复杂度为O(log n),其中log n表示递归调用的栈空间。
稳定性:
快速排序是一种不稳定的排序算法。不稳定性主要表现在基准元素的选择和交换过程中可能破坏相等元素的相对顺序。
快速排序的特点:
- 高效性能: 在平均情况下,快速排序的性能较好,比许多排序算法更快。
- 原地排序: 快速排序是一种原地排序算法,不需要额外的存储空间。
- 适用于大规模数据: 在处理大规模数据集时,快速排序通常具有较好的性能。
快速排序的优势:
- 快速排序是一种高效的排序算法,特别适用于大规模数据集的排序。
- 在实践中,快速排序通常比其他高级排序算法更快。
注意事项:
- 快速排序的性能高度依赖于基准元素的选择,不同的基准选择策略会影响算法的性能。
- 在最坏情况下,快速排序的时间复杂度会达到O(n^2),此时基准的选择不当,例如选择了已经有序的序列中的最小元素。
- 为了提高性能,通常对小规模子数组采用其他排序算法,例如插入排序。这种优化称为快速排序的优化版本,通常称为快速排序的“三数取中”优化。
代码
package 排序算法.快排;
public class QuickSort {
public static void quickSort(int[] arr) {
if (arr == null||arr.length==1) {
return;
}
proscss(arr,0,arr.length-1);
}
/**
* 快速排序
* @param arr
* @param l
* @param r
*/
public static void proscss(int[] arr,int l ,int r){
//跳出机制
if (l >= r) {
return;
}
//排序当前 分为 小于 等于 大与 并返回对应下标志
int a[] = sort1(arr,l,r);
printArray(arr);
//递归继续排序小于
proscss(arr,l,a[0]);
//递归继续排序大与
proscss(arr,a[1],r);
}
/**
* 具体的拆分,实现 按照R为标杆,p1左边是小数 p1-p2中间是相等数 p2-r是大与的数
* @param arr
* @param l
* @param r
* @return
*/
private static int[] sort1(int[] arr, int l, int r) {
System.out.println("l="+l+"r="+r);
//跳出
if(l>=r){
return new int[]{l,r};
}
//如果是2个就直接排序
if(r-l==1){
if (arr[l] > arr[r]) {
swap(arr,l,r);
}
return new int[]{l,r};
}
int p1 = l;
int p2 = r-1;//这里从r-1开始算 r作为中间数
int i = l;
//i 是判断的,直到p2 p2 右边是大与R的数 是已经判断交换过的
while (i<=p2){
//如果i大与于r 就放到大与于堆里
if (arr[i]>arr[r] ){
//i与 p2 交换 p2 --
swap(arr,i,p2);
p2--;
continue;
}
//如果i小于r 放到小于的堆里
if (arr[i]<arr[r] ){
//交换第i个与p1 p1++
swap(arr,i,p1);
p1++;
i++;
continue;
}
//如果i等于r 就放到等于堆里
if (arr[i]==arr[r] ){
i++;
continue;
}
}
//将r放到中间
swap(arr,p2+1,r);
p2++;
int[] rt = new int[2];
rt[0]=p1-1;
rt[1]=p2+1;
return rt;
}
public static void swap(int[] arr,int i,int j){
if ( i== j) {
return;
}
// System.out.println("i="+i+"j="+j);
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
static void printArray(int[] arr) {
for (int value : arr) {
System.out.print(value + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = new int[]{1,6,5,2,7,5,3,8,4,5};
quickSort(arr);
System.out.println(arr.toString());
}
}
3.5归并排序算法(Merge Sort)
排序方式:
归并排序是一种分治思想的排序算法,其基本思想是将待排序序列分为若干个子序列,对每个子序列进行排序,然后合并这些有序的子序列,直到整个序列有序。具体步骤如下:
- 将待排序序列分成两个子序列。
- 对每个子序列递归进行归并排序。
- 将两个有序的子序列合并为一个有序序列。
时间复杂度:
- 最坏时间复杂度:O(n log n)
- 平均时间复杂度:O(n log n)
- 最好时间复杂度:O(n log n)
归并排序的时间复杂度相对稳定,无论输入数据的分布如何,其性能表现都较为优异。
空间复杂度:
归并排序是一种稳定的排序算法,但其空间复杂度较高,为O(n),其中n为待排序序列的长度。归并排序需要额外的空间来存储合并过程中的临时数组。
稳定性:
归并排序是一种稳定的排序算法。稳定性是指在排序过程中,相等元素的相对顺序不发生改变。
归并排序的特点:
- 稳定性: 归并排序是一种稳定的排序算法,适用于对相等元素保持相对顺序的场景。
- 适用于链表: 归并排序对于链表的操作较为简便,因此在链表上的性能通常较好。
- 适用于大规模数据: 归并排序在处理大规模数据集时性能较好,且其空间复杂度相对较高的问题在实际应用中有一定的补救方法。
归并排序的优势:
- 归并排序是一种高效的排序算法,特别适用于大规模数据集的排序。
- 归并排序是一种稳定的排序算法,适用于需要保持相等元素相对顺序的场景。
注意事项:
- 归并排序的主要缺点是其空间复杂度较高,需要额外的空间来存储合并过程中的临时数组。在空间有限的情况下,可能需要考虑其他排序算法。
- 归并排序通常在递归实现时使用额外的递归调用栈,因此对于递归深度较大的情况,可能会受到栈空间的限制。
代码
public class MergeSort {
// 归并排序主函数
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int[] temp = new int[arr.length]; // 临时数组用于存储归并过程中的中间结果
mergeSort(arr, 0, arr.length - 1, temp);
}
// 递归实现归并排序
private static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid, temp); // 对左半部分进行归并排序
mergeSort(arr, mid + 1, right, temp); // 对右半部分进行归并排序
merge(arr, left, mid, right, temp); // 合并左右两部分
}
}
// 合并两个有序部分
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左半部分的起始位置
int j = mid + 1; // 右半部分的起始位置
int k = 0; // 临时数组的起始位置
// 将两个有序部分合并到临时数组中
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 将左半部分剩余的元素复制到临时数组
while (i <= mid) {
temp[k++] = arr[i++];
}
// 将右半部分剩余的元素复制到临时数组
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组的元素复制回原数组
k = 0;
while (left <= right) {
arr[left++] = temp[k++];
}
}
// 打印数组元素
public static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = {38, 27, 43, 3, 9, 82, 10};
System.out.println("原始数组:");
printArray(arr);
mergeSort(arr);
System.out.println("归并排序后的数组:");
printArray(arr);
}
}
3.6希尔排序(Shell Sort)
排序方式:
希尔排序是插入排序的改进版本,也称为缩小增量排序。其基本思想是通过将待排序序列分割成若干个子序列,对每个子序列进行插入排序,不断减小增量(间隔),最终使整个序列成为有序。具体步骤如下:
- 选择一个增量序列,将待排序序列分割成若干个子序列,每个子序列的元素间隔为增量。
- 对每个子序列进行插入排序。
- 不断减小增量,重复步骤2,直至增量为1,完成最后一次插入排序。
在希尔排序中,gap
是步长(增量)的概念,它表示在每一轮排序中,待排序序列中的元素之间的间隔。希尔排序的核心思想是通过逐步减小步长,对分组后的子序列进行插入排序,最终使整个序列达到有序状态。
具体来说,希尔排序的算法步骤如下:
- 选择一个初始步长
gap
。 - 将序列分割成若干个子序列,每个子序列包含相隔
gap
的元素。 - 对每个子序列进行插入排序。
- 逐步减小步长
gap
,重复步骤2和步骤3,直至步长gap
缩小为1,最后进行一次插入排序。
在每一轮排序中,gap
决定了每个子序列中相邻元素的间隔,通过不断减小 gap
的方式,可以使得整个序列逐渐变得有序。希尔排序的性能与步长序列的选择有关,通常使用一些预先定义好的增量序列,如希尔增量序列(Shell's increments)或 Knuth 增量序列。
例如,希尔增量序列的计算方式为:gap = gap / 2
,一开始的初始 gap
可以设为数组长度的一半。这个增量序列是一个常用的选择,但也可以根据实际情况尝试其他增量序列。
时间复杂度:
- 最坏时间复杂度:取决于增量序列,一般为O(n^2)。
- 平均时间复杂度:取决于增量序列,一般为O(n^1.3)。
- 最好时间复杂度:O(n log n)。
希尔排序的时间复杂度与增量序列的选择有关,较为复杂。通常采用一些常用的增量序列,如希尔增量序列,Knuth 增量序列等。
空间复杂度:
希尔排序是一种原地排序算法,其空间复杂度为O(1)。
稳定性:
希尔排序是一种不稳定的排序算法。不稳定性是指在排序过程中,相等元素的相对顺序可能发生改变。
希尔排序的特点:
- 希尔排序是插入排序的改进版本,通过分割子序列和较大的步长进行排序,逐步减小步长,提高了效率。
- 希尔排序的性能依赖于增量序列的选择,不同的增量序列可能导致不同的性能表现。
希尔排序的优势:
- 希尔排序在某些情况下的性能优于直接插入排序,尤其是对于较大规模的数据。
- 希尔排序是原地排序算法,空间复杂度相对较低。
注意事项:
- 增量序列的选择对希尔排序的性能影响较大,不同的增量序列可能导致不同的时间复杂度。
- 希尔排序的稳定性较差,对于要求相等元素的相对顺序不变的场景,可能不适用。
代码
public class ShellSort {
public static void main(String[] args) {
int[] array = {64, 34, 25, 12, 22, 11, 90};
System.out.println("排序前数组:");
printArray(array);
shellSort(array);
System.out.println("\n排序后数组:");
printArray(array);
}
// 希尔排序算法
static void shellSort(int[] arr) {
int n = arr.length;
// 初始步长设定为数组长度的一半,然后逐步缩小步长
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个步长进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
// 在当前步长下,进行插入排序
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
// 将当前元素插入到正确的位置
arr[j] = temp;
}
}
}
// 打印数组元素
static void printArray(int[] arr) {
for (int value : arr) {
System.out.print(value + " ");
}
}
}
3.7堆排序(Heap Sort)
介绍:
堆排序是一种基于二叉堆数据结构的排序算法。二叉堆是一种特殊的树形数据结构,分为最大堆和最小堆。在最大堆中,每个节点的值都大于或等于其子节点的值;而在最小堆中,每个节点的值都小于或等于其子节点的值。
排序方法和结构:
堆排序的基本思想是通过构建二叉堆,将待排序序列构建成一个堆,然后依次将堆顶元素(最大元素或最小元素)取出,与堆中最后一个元素交换位置,再重新调整堆,直至整个序列有序。
堆排序的主要操作包括:
- 建堆(Heapify): 将一个无序序列构建成一个堆,分为自底向上和自顶向下两种方式。
- 堆调整(HeapifyDown 或 HeapifyUp): 在移除堆顶元素后,调整剩余元素使之仍然满足堆的性质。
- 堆排序(HeapSort): 不断移除堆顶元素,并进行堆调整,直到整个序列有序。
操作复杂度:
- 建堆时间复杂度: O(n),其中 n 为待排序序列的长度。
- 堆调整时间复杂度: O(log n),其中 n 为堆的大小。
- 堆排序时间复杂度: O(n log n),建堆和每次堆调整的总和。
- 空间复杂度: O(1),堆排序是一种原地排序算法。
优缺点:
-
优点:
- 堆排序是一种原地排序算法,不需要额外的辅助空间。
- 相对于冒泡排序和插入排序等简单排序算法,堆排序的时间复杂度较低,尤其适用于大规模数据的排序。
- 堆排序是一种稳定的排序算法,对相同元素的排序结果不会改变它们的相对顺序。
-
缺点:
- 堆排序的常数因子较大,相对于快速排序等算法,性能可能较差。
- 不适用于链式存储结构,因为它需要随机访问元素。
- 对于小规模数据,其他简单排序算法可能更为高效。
实现步骤:
- 构建一个最大堆(建堆)。
- 将堆顶元素与堆的最后一个元素交换位置。
- 堆的大小减一,对堆顶元素进行堆调整(HeapifyDown)。
- 重复步骤2和步骤3,直至堆的大小为1。
代码
package 算法;
public class 堆排序 {
public static class MyMaxHeap {
int[] heap;//存储堆的数据
int size;//当前堆的数量
int limit;//堆的最大数量
public MyMaxHeap(int limit) {
this.heap = new int[limit];
this.size = 0;
this.limit = limit;
}
//放到堆里
public void push(int value){
//1 判断堆是不是满了
if(size==limit){
throw new RuntimeException("heap is full");
}
//2 没满就放到数组的最后面
heap[size]=value;
size++;
//3 找到对应的父节点,比较,如果大就交换( 父节点下标=(当前节点下标-1) 除2)
int nowNode = size-1;
while (heap[nowNode] > heap[(nowNode-1)/2]){
//交换
swap(heap,nowNode,(nowNode-1)/2);
//当前下标变更
nowNode = (nowNode-1)/2;
}
}
//从堆里取出
public int pop(){
//1 判断是否还有数据
if (size==0) {
throw new RuntimeException("heap is null");
}
// 2 将堆顶的数据推出
int index = 0;
int ans = heap[index];
// 3 将堆尾的最后一个数据放到堆顶 因为size 是个数 这里交换的是下标 如果是10个下标应该是9 而个数减了刚好是9就写成了--9
swap(heap,index,--size);
// 4 将推顶上的数向下对比,如果比下面小就交换
int left = 1; //左叶子节点 = 当前节点 * 2 +1 右叶子节点 = 当前节点 * 2 + 2
while (left < size){
//如果left < size 说明当前节点有叶子节点
//判断是否有右边的叶子节点,如果有 就找出2个节点最大的数和
int lage = left+1< size ?
heap[left]<heap[left+1]?
left+1: left
:left;
//拿最大的数和当前节点比 如果当前节点大就停止
if (heap[lage] < heap[index]) {
break;
}
//交换
swap(heap,lage,index);
index = lage;
//计算当前节点的左子树
left = index * 2 +1;
}
return ans;
}
private void swap(int[] arr, int i, int j) {
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
}
}
public static void main(String[] args) {
MyMaxHeap myMaxHeap = new MyMaxHeap(10);
myMaxHeap.push(4);
myMaxHeap.push(5);
myMaxHeap.push(1);
myMaxHeap.push(6);
myMaxHeap.push(2);
myMaxHeap.push(7);
myMaxHeap.push(3);
myMaxHeap.push(8);
myMaxHeap.push(9);
myMaxHeap.push(10);
for (int i = 0; i < 10; i++) {
System.out.println(String.valueOf(myMaxHeap.pop()));
}
}
}
3.8计数排序算法(Counting Sort)
排序方式:
计数排序是一种非比较排序算法,其基本思想是统计待排序序列中每个元素的出现次数,然后根据统计信息重构有序序列。具体步骤如下:
- 统计待排序序列中每个元素的出现次数,得到一个计数数组(Counting Array)。
- 对计数数组进行累加,得到每个元素在有序序列中的最终位置。
- 根据计数数组的信息,将待排序序列的元素放置到有序序列中的相应位置。
时间复杂度:
- 最坏时间复杂度:O(n + k)
- 平均时间复杂度:O(n + k)
- 最好时间复杂度:O(n + k)
其中,n 表示待排序序列的长度,k 表示待排序序列中元素的取值范围(即最大值与最小值之差加一)。
计数排序的时间复杂度较低,是一种效率较高的排序算法。
空间复杂度:
计数排序是一种非原地排序算法,其空间复杂度为O(k),其中 k 表示待排序序列中元素的取值范围。
稳定性:
计数排序是一种稳定的排序算法。稳定性是指在排序过程中,相等元素的相对顺序不发生改变。
计数排序的特点:
- 适用于整数排序: 计数排序主要适用于非负整数排序,且适用于元素取值范围不太大的情况。
- 不适用于浮点数排序: 由于计数排序依赖于整数的计数,不适用于浮点数的排序。
- 适用于重复元素较多的场景: 计数排序对于具有大量重复元素的序列表现较好。
计数排序的优势:
- 计数排序是一种非比较排序算法,其时间复杂度相对较低,适用于整数排序。
- 对于具有大量重复元素的序列,计数排序的性能优势更为显著。
注意事项:
- 计数排序对于元素的取值范围要求较为严格,如果元素的取值范围较大,可能导致计数数组的空间开销过大。
- 计数排序不适用于负整数的排序,但可以通过一些变换来应对。
代码
import java.util.Arrays;
public class CountSort {
public static void countSort(int[] num){
if (num == null || num.length <=1) {
return;
}
//1 找出最大数建立数组
int max =0;
for (int i = 0; i < num.length ; i++) {
max = Math.max(max,num[i]);
}
//2 新建数组 计数
int helpArr[] = new int[max+1];
for (int i = 0; i < num.length; i++) {
helpArr[num[i]]++;
}
//3 输出对应的数
int j = 0;
for (int i = 0; i < helpArr.length; i++) {
//如果>0就一直输出 直到等于0 就输出下一个
while (helpArr[i]-- >0){
num[j++] =i;
}
}
}
private static int[] getArr(int maxValue,int maxSize ) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random());
}
return arr;
}
public static void main(String[] args) {
//生产数组
int[] arr = getArr(1000, 1000);
//计数排序
countSort(arr);
}
}
3.9基数排序算法(Radix Sort)
排序方式:
基数排序是一种非比较排序算法,其基本思想是从低位到高位依次对待排序的整数进行排序,每一轮排序根据某一位的值将元素分配到不同的桶中,然后按照桶的顺序重构序列。具体步骤如下:
- 从最低位开始,按照每一位的值进行排序,可以使用计数排序等稳定排序算法。
- 依次对所有位进行排序,最终得到有序序列。
时间复杂度:
- 最坏时间复杂度:O(d * (n + k))
- 平均时间复杂度:O(d * (n + k))
- 最好时间复杂度:O(d * (n + k))
其中,n 表示待排序序列的长度,d 表示最大元素的位数,k 表示每一位的取值范围。
基数排序的时间复杂度相对较低,但其性能在某些场景下可能不如其他排序算法。
空间复杂度:
基数排序是一种非原地排序算法,其空间复杂度为O(n + k),其中 n 表示待排序序列的长度,k 表示每一位的取值范围。
稳定性:
基数排序是一种稳定的排序算法。稳定性是指在排序过程中,相等元素的相对顺序不发生改变。
基数排序的特点:
- 适用于整数排序: 基数排序主要适用于整数的排序,且对于整数的每一位使用稳定排序算法。
- 适用于位数较小的整数: 基数排序的性能与元素的位数有关,当位数较小且元素分布较均匀时,性能较好。
- 不适用于负整数和浮点数: 基数排序通常不直接适用于负整数和浮点数的排序,需要进行适当的处理。
基数排序的优势:
- 基数排序是一种非比较排序算法,相对于比较排序算法,它的时间复杂度较低。
- 在某些特定场景下,基数排序的性能可能优于其他排序算法,尤其是对于位数较小的整数。
注意事项:
- 基数排序对于每一位的排序使用稳定的排序算法,通常会选择计数排序或桶排序作为子排序算法。
- 基数排序的性能高度依赖于元素的位数,当位数较大时,性能可能不如其他排序算法。
代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RadixSort {
public static void radixSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int max = getMaxValue(arr);
// 对每一位进行计数排序
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
// 获取数组中的最大值
private static int getMaxValue(int[] arr) {
int max = arr[0];
for (int num : arr) {
if (num > max) {
max = num;
}
}
return max;
}
// 使用计数排序对数组按照某一位进行排序
private static void countingSortByDigit(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];
// 统计每个数字的出现次数
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
// 对计数数组进行累加
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 从后往前遍历原数组,根据计数数组得到有序数组
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// 将有序数组复制回原数组
System.arraycopy(output, 0, arr, 0, n);
}
// 打印数组元素
public static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
System.out.println("原始数组:");
printArray(arr);
radixSort(arr);
System.out.println("基数排序后的数组:");
printArray(arr);
}
}
3.10 桶排序(Bucket Sort)
介绍:
桶排序是一种分布式排序算法,它将元素分散到若干个桶中,然后分别对每个桶进行排序,最后按照顺序将所有桶的元素合并得到有序序列。桶排序的性能依赖于桶的划分和各个桶内部的排序算法。
排序方法和结构:
桶排序的基本步骤如下:
- 将待排序元素均匀地分散到若干个桶中。
- 对每个桶内的元素进行排序,可以选择不同的排序算法,甚至递归使用桶排序。
- 将各个桶中的元素按照顺序合并得到有序序列。
操作复杂度:
- 桶的划分时间复杂度: O(n + k),其中 n 为待排序元素个数,k 为桶的个数。
- 桶内排序时间复杂度: 取决于采用的排序算法,一般为 O(n log n)。
- 桶合并时间复杂度: O(n + k),其中 n 为待排序元素个数,k 为桶的个数。
- 总体时间复杂度: 取决于桶内排序的时间复杂度,通常为 O(n + k) 到 O(n^2)。
优缺点:
-
优点:
- 桶排序是一种分布式排序算法,适用于外部排序。
- 在元素均匀分布的情况下,桶排序具有较好的性能。
- 可以适用于桶内元素数量较小的场景,甚至可以递归使用桶排序。
-
缺点:
- 桶排序的性能依赖于桶的划分和各个桶内部排序算法的选择。
- 对于不均匀分布的元素,桶排序的性能可能下降。
- 需要额外的存储空间,因为需要维护若干个桶。
实现步骤:
- 确定桶的个数,将待排序元素均匀地分散到各个桶中。
- 对每个桶内的元素进行排序,可以选择不同的排序算法。
- 将各个桶内的元素按照顺序合并得到有序序列。
代码
以下是桶排序的简单示例代码,假设待排序元素均为非负整数:
import java.util.ArrayList;
import java.util.Collections;
public class BucketSort {
public static void bucketSort(int[] arr) {
int max = getMaxValue(arr);
int bucketCount = (int) Math.sqrt(arr.length); // 桶的个数,可以根据实际情况调整
// 创建桶
ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 将元素分散到桶中
for (int num : arr) {
int index = (int) ((double) num / max * (bucketCount - 1));
buckets.get(index).add(num);
}
// 对每个桶内的元素进行排序(使用 Collections.sort 可以选择不同的排序算法)
for (ArrayList<Integer> bucket : buckets) {
Collections.sort(bucket);
}
// 合并各个桶的元素
int index = 0;
for (ArrayList<Integer> bucket : buckets) {
for (int num : bucket) {
arr[index++] = num;
}
}
}
// 获取数组中的最大值
private static int getMaxValue(int[] arr) {
int max = arr[0];
for (int num : arr) {
if (num > max) {
max = num;
}
}
return max;
}
// 打印数组元素
private static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = {29, 10, 14, 37, 13};
System.out.println("原始数组:");
printArray(arr);
}
}
4 二分法查找
二分查找是一种高效的查找算法,它基于分治思想,通过将有序数组分为两半,并在每一步中缩小搜索范围,从而快速定位目标值。
算法逻辑:
- 初始状态: 在有序数组中选择中间元素。
- 比较: 将目标值与中间元素进行比较。
- 调整搜索范围:
- 如果目标值等于中间元素,查找成功。
- 如果目标值小于中间元素,说明目标值在左半部分,将搜索范围缩小为左半部分。
- 如果目标值大于中间元素,说明目标值在右半部分,将搜索范围缩小为右半部分。
- 重复: 在新的搜索范围中重复以上步骤,直到找到目标值或搜索范围为空。
应用场景:
- 有序数组或列表: 二分查找适用于有序的数据结构,如有序数组或有序链表。
- 静态数据: 适用于静态(不经常变动)的数据集合。
时间复杂度:
二分查找的时间复杂度为O(log n),其中n是数组的长度。这是因为每一步都将搜索范围减半。
优势与劣势:
优势:
- 高效性: 二分查找是一种高效的查找算法,特别适用于大型有序数据集合。
- 简单: 算法逻辑相对简单,容易理解和实现。
劣势:
- 仅适用于有序数据: 二分查找要求数据集合是有序的,如果数据未排序,需要预处理为有序。
- 不适用于动态数据: 如果数据集合经常变动,例如频繁插入或删除操作,二分查找的维护代价较高。
总体而言,二分查找在满足有序数据的前提下,是一种高效的查找算法。在静态数据集合中,它通常比线性查找更具有优势。
代码
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5,6,7,8,9,10};
System.out.println(find(arr,0,arr.length,4));
}
public static int find(int[] arr ,int l, int r ,int num){
if (r-l <=1) {
if (arr[r]==num) {
return r;
}
if (arr[l]==num) {
return l;
}
return -1;
}
int flag = l + ((r - l) >> 1);
if (arr[flag] == num) {
return flag;
}
if (arr[flag] > num) {
return find(arr,l,flag-1,num);
}else{
return find(arr,flag+1,r,num);
}
}