数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。每一种数据结构都有着独特的数据存储方式。常用的数据结构有:数组,栈,链表,队列,树,图,堆,散列表
数组(顺序表)
数组是一种线性数据结构,用于存储一组具有相同数据类型的元素。数组中的元素按照一定的顺序排列,并且可以通过索引(下标)来访问每个元素。
特点
-
元素类型相同:数组中的所有元素必须具有相同的数据类型,比如整数、浮点数、字符等。
-
连续的内存:数组的元素在内存中是连续存储的,这样可以通过索引计算出元素的地址,并关切支持常数时间的随机访问。
-
固定大小:数组的大小在创建时就确定,并且在整个生命周期中保持不变。如果需要存储更多的元素,需要重新创建一个更大的数组
基本操作
直接上代码
#include <iostream>
int main() {
// 创建一个整数数组,大小为5
int array[5];
// 向数组中插入元素
array[0] = 10;
array[1] = 20;
array[2] = 30;
array[3] = 40;
array[4] = 50;
// 访问数组中的元素
std::cout << "array[2]: " << array[2] << std::endl;
// 修改数组中的元素
array[2] = 35;
// 删除数组中的元素,通过移动元素来删除下标为2的元素
for (int i = 2; i < 4; ++i) {
array[i] = array[i+1];
}
// 访问修改后的数组中的元素
std::cout << "array[2]: " << array[2] << std::endl;
// 插入元素,将元素35插入到下标为2的位置
for (int i = 4; i > 2; --i) {
array[i] = array[i-1];
}
array[2] = 35;
// 访问插入后的数组中的元素
std::cout << "array[2]: " << array[2] << std::endl;
return 0;
}
在C++中,数组可以分为静态数组(Static Array)和动态数组(Dynamic Array)两种类型。
- 静态数组:
静态数组是在编译时就确定大小的数组,其大小是固定的。在声明静态数组时,需要指定数组的大小,并且该大小在整个程序运行期间不可改变。静态数组在栈上分配内存空间。
示例代码:
// 声明一个静态数组,大小为5
int staticArray[5];
// 初始化静态数组
staticArray[0] = 10;
staticArray[1] = 20;
staticArray[2] = 30;
staticArray[3] = 40;
staticArray[4] = 50;
- 动态数组:
动态数组是在运行时动态分配内存的数组,其大小可以根据需要进行改变。动态数组使用new
操作符来创建,在堆上分配内存空间。通过delete
操作符来释放动态数组占用的内存空间。
示例代码:
// 创建一个动态数组,大小为5
int* dynamicArray = new int[5];
// 初始化动态数组
dynamicArray[0] = 10;
dynamicArray[1] = 20;
dynamicArray[2] = 30;
dynamicArray[3] = 40;
dynamicArray[4] = 50;
// 删除动态数组
delete[] dynamicArray;
动态数组的大小可以通过重新分配内存来改变,可以使用new
操作符来创建新的更大的数组,将原数组的元素复制到新数组中,然后删除原数组。
需要注意的是,在使用动态数组时,需要手动释放内存,否则可能会导致内存泄漏。使用delete[]
来释放动态数组占用的内存空间。
链表
链表由一系列节点组成,每个节点都包含两个部分:数据域(Data)和指针域(
Pointer)。数据域存储具体的数据,指针域指向下一个节点(或前一个节点,在双向链表中)的地址。
[Node1] -> [Node2] -> [Node3] -> ... -> [NodeN]
特点:
-
非连续的内存存储:链表的结点在内存中不是连续存储的,每个节点通过指针链接到下一个节点,因此插入和删除操作相对容易。
-
动态大小:链表的大小可以根据需要动态调整,可以在运行时动态插入和删除节点。
-
灵活的插入和删除:由于链表的结点通过指针链接,插入和删除节点只需要改变指针的指向,而不需要移动其他结点,因此时间复杂度为O(1)。
-
随机访问的复杂度高:链表需要遍历整个链表才能访问特顶位置的结点,因此随机访问的时间复杂度为O(n)。
基本操作:
直接上代码
#include <iostream>
// 定义链表节点结构
struct Node {
int data; // 数据域
Node* next; // 指针域,指向下一个节点
};
// 头部插入
void insertAtHead(Node* &head, int value) {
Node* newNode = new Node(); // 创建新节点
newNode->data = value; // 设置新节点的数据
newNode->next = head; // 将指针域指向旧的头节点
head = newNode; // 将头节点指向新节点
}
// 尾部插入
void insertAtTail(Node* &head, int value) {
Node* newNode = new Node(); // 创建新节点
newNode->data = value; // 设置新节点的数据
newNode->next = nullptr; // 将指针域指向空
if (head == nullptr) { // 若链表为空,则新节点为头节点
head = newNode;
return;
}
Node* temp = head;
while (temp->next != nullptr) { // 找到链表的最后一个节点
temp = temp->next;
}
temp->next = newNode; // 将最后一个节点的指针域指向新节点
}
// 遍历链表并打印节点数据
void printList(Node* head) {
Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " ";
temp = temp->next;
}
std::cout << std::endl;
}
// 删除指定节点
void deleteNode(Node* &head, int value) {
if (head == nullptr) {
return;
}
// 若待删除的节点为头节点,更新头指针
if (head->data == value) {
Node* temp = head;
head = head->next;
delete temp;
return;
}
Node* temp = head;
while (temp->next != nullptr && temp->next->data != value) {
temp = temp->next;
}
if (temp->next == nullptr) {
// 未找到要删除的节点
return;
}
Node* toDelete = temp->next;
temp->next = temp->next->next;
delete toDelete;
}
int main() {
Node* head = nullptr;
// 头部插入示例
insertAtHead(head, 10);
insertAtHead(head, 20);
std::cout << "List after head insertion: ";
printList(head); // 输出:20 10
// 尾部插入示例
insertAtTail(head, 30);
insertAtTail(head, 40);
std::cout << "List after tail insertion: ";
printList(head); // 输出:20 10 30 40
// 删除节点示例
deleteNode(head, 10);
std::cout << "List after deletion: ";
printList(head); // 输出:20 30 40
return 0;
}
链表常见类型
- 单向链表(Singly Linked List):每个节点只有一个指针,指向下一个节点。
- 双向链表(Doubly Linked List):每个节点有两个指针,分别指向前一个节点和后一个节点。
- 循环链表(Circular Linked List):链表的最后一个节点指向头节点,形成一个循环结构。
双向链表基本操作
#include <iostream>
// 定义双向链表节点结构
struct Node {
int data; // 数据域
Node* prev; // 指向前一个节点的指针
Node* next; // 指向下一个节点的指针
};
// 头部插入
void insertAtHead(Node* &head, int value) {
Node* newNode = new Node(); // 创建新节点
newNode->data = value; // 设置新节点的数据
newNode->prev = nullptr; // 将前一个节点指针域指向空
newNode->next = head; // 将后一个节点指针域指向旧的头节点
if (head != nullptr) {
head->prev = newNode; // 更新旧的头节点的前一个节点指针域
}
head = newNode; // 将头节点指向新节点
}
// 尾部插入
void insertAtTail(Node* &head, int value) {
Node* newNode = new Node(); // 创建新节点
newNode->data = value; // 设置新节点的数据
newNode->next = nullptr; // 将后一个节点指针域指向空
if (head == nullptr) { // 若链表为空,则新节点为头节点
newNode->prev = nullptr; // 将前一个节点指针域指向空
head = newNode;
return;
}
Node* temp = head;
while (temp->next != nullptr) { // 找到链表的最后一个节点
temp = temp->next;
}
temp->next = newNode; // 将最后一个节点的指针域指向新节点
newNode->prev = temp; // 更新新节点的前一个节点指针域
}
// 遍历链表并打印节点数据
void printList(Node* head) {
Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " ";
temp = temp->next;
}
std::cout << std::endl;
}
// 删除指定节点
void deleteNode(Node* &head, int value) {
if (head == nullptr) {
return;
}
Node* temp = head;
// 查找要删除的节点
while (temp != nullptr && temp->data != value) {
temp = temp->next;
}
if (temp == nullptr) {
// 未找到要删除的节点
return;
}
// 判断要删除的节点是否为头节点
if (temp == head) {
head = head->next;
} else {
temp->prev->next = temp->next;
if (temp->next != nullptr) {
temp->next->prev = temp->prev;
}
}
delete temp; // 释放内存
}
int main() {
Node* head = nullptr;
// 头部插入示例
insertAtHead(head, 10);
insertAtHead(head, 20);
std::cout << "List after head insertion: ";
printList(head); // 输出:20 10
// 尾部插入示例
insertAtTail(head, 30);
insertAtTail(head, 40);
std::cout << "List after tail insertion: ";
printList(head); // 输出:20 10 30 40
// 删除节点示例
deleteNode(head, 10);
std::cout << "List after deletion: ";
printList(head); // 输出:20 30 40
return 0;
}
循环链表基本操作
-
插入节点:
void insert(Node* &head, int value) { Node* newNode = new Node(); // 创建新节点 newNode->data = value; // 设置新节点的数据 if (head == nullptr) { // 若循环列表为空 newNode->next = newNode; // 将新节点的指针指向自身 head = newNode; // 将头节点指向新节点 } else { newNode->next = head->next; // 将新节点的指针指向第一个节点 head->next = newNode; // 将头节点的指针指向新节点 head = newNode; // 将新节点设为头节点 } }
-
删除节点:
void deleteNode(Node* &head, int value) { if (head == nullptr) { return; // 若循环列表为空,则直接返回 } Node* current = head; // 从头节点开始遍历 Node* prev = nullptr; // 记录当前节点的前一个节点 // 寻找要删除的节点 do { if (current->data == value) { break; // 找到了要删除的节点 } prev = current; current = current->next; } while (current != head); if (current == head && current != nullptr) { head = head->next; // 若删除的是头节点,则更新头节点 } // 删除节点 if (current != nullptr) { prev->next = current->next; // 将前一个节点的指针指向下一个节点 delete current; // 释放节点的内存 } }
-
遍历列表并打印节点数据:
void printList(Node* head) { if (head == nullptr) { return; } Node* temp = head; do { std::cout << temp->data << " "; temp = temp->next; } while (temp != head); std::cout << std::endl; }
栈(后进先出)
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端成为栈顶,另一端成为栈底。栈中的数据元素遵守后进先出的原则。
入栈: 将元素压入栈顶。
-
新元素被放置在栈顶位置。
-
如果栈是空的,那么心愿就成为栈的唯一元素
出栈:
-
栈顶元素被移出栈。
-
如果栈只有一个元素,那么栈将变为空。
栈通常使用数组或链表来实现。
顺序栈
const int MAX_SIZE = 100; // 栈的最大容量
class Stack {
private:
int arr[MAX_SIZE]; // 数组存储栈元素
int top; // 栈顶指针
public:
Stack() {
top = -1; // 初始化栈顶指针为-1,表示空栈
}
void push(int val) {
if (top >= MAX_SIZE - 1) {
std::cout << "Stack Overflow" << std::endl;
return;
}
arr[++top] = val; // 栈顶指针加1,将元素放入栈顶
}
void pop() {
if (isEmpty()) {
std::cout << "Stack Underflow" << std::endl;
return;
}
top--; // 栈顶指针减1,表示弹出了栈顶元素
}
int peek() {
if (isEmpty()) {
std::cout << "Stack is empty" << std::endl;
return -1;
}
return arr[top]; // 返回栈顶元素的值
}
bool isEmpty() {
return top == -1; // 判断栈是否为空
}
};
链栈
class Node {
public:
int data; // 存储数据
Node* next; // 指向下一个节点
Node(int val) {
data = val;
next = nullptr;
}
};
class Stack {
private:
Node* top; // 栈顶指针
public:
Stack() {
top = nullptr; // 初始化栈顶指针为空
}
void push(int val) {
Node* newNode = new Node(val); // 创建新节点
newNode->next = top; // 将新节点的next指针指向栈顶节点
top = newNode; // 将新节点设为栈顶节点
}
void pop() {
if (isEmpty()) {
std::cout << "Stack Underflow" << std::endl;
return;
}
Node* temp = top; // 临时存储栈顶节点
top = top->next; // 栈顶指针指向下一个节点
delete temp; // 释放临时节点的内存
}
int peek() {
if (isEmpty()) {
std::cout << "Stack is empty" << std::endl;
return -1;
}
return top->data; // 返回栈顶元素的值
}
bool isEmpty() {
return top == nullptr; // 判断栈是否为空
}
};
栈可用于解决许多问题,如表达式求值、括号匹配、深度优先搜索等。它提供了一种方便的方式来管理数据,并具有高效的插入和删除操作。
队列
队列只允许在一端进行数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出的特点。
入队: 将元素添加到队列的末尾
-
新元素被添加到队列的末尾,成为新的队尾
-
如果队列是空的,那么新元素即是队头也是队尾
出队: 从队列的头部移出一个元素
-
对头元素被移出队列
-
如果只有一个元素,队列将为空
队列通常使用数组或链表来实现。
使用数组实现队列:
const int MAX_SIZE = 100; // 队列的最大容量
class Queue {
private:
int arr[MAX_SIZE]; // 数组存储队列元素
int front; // 队头指针
int rear; // 队尾指针
public:
Queue() {
front = -1; // 初始化队头指针为-1
rear = -1; // 初始化队尾指针为-1
}
void enqueue(int val) {
if (rear >= MAX_SIZE - 1) {
std::cout << "Queue Overflow" << std::endl;
return;
}
arr[++rear] = val; // 队尾指针加1,将元素放入队尾
if (front == -1) {
front = 0; // 如果队列是空的,将队头指针设为0
}
}
void dequeue() {
if (isEmpty()) {
std::cout << "Queue Underflow" << std::endl;
return;
}
if (front == rear) {
front = -1; // 如果队列只有一个元素,将队头指针和队尾指针都设为-1,表示队列为空
rear = -1;
} else {
front++; // 队头指针加1,表示移除了队头元素
}
}
int peek() {
if (isEmpty()) {
std::cout << "Queue is empty" << std::endl;
return -1;
}
return arr[front]; // 返回队头元素的值
}
bool isEmpty() {
return front == -1; // 判断队列是否为空
}
};
使用链表实现队列
class Node {
public:
int data; // 存储数据
Node* next; // 指向下一个节点
Node(int val) {
data = val;
next = nullptr;
}
};
class Queue {
private:
Node* front; // 队头指针
Node* rear; // 队尾指针
public:
Queue() {
front = nullptr; // 初始化队头指针为空
rear = nullptr; // 初始化队尾指针为空
}
void enqueue(int val) {
Node* newNode = new Node(val); // 创建新节点
if (rear == nullptr) {
front = newNode; // 如果队列是空的,将队头指针指向新节点
} else {
rear->next = newNode; // 否则,将队尾节点的next指针指向新节点
}
rear = newNode; // 将新节点设为队尾节点
}
void dequeue() {
if (isEmpty()) {
std::cout << "Queue Underflow" << std::endl;
return;
}
Node* temp = front; // 临时存储队头节点
front = front->next; // 队头指针指向下一个节点
delete temp; // 释放临时节点的内存
if (front == nullptr) {
rear = nullptr; // 如果队列只有一个元素,将队
哈希表(散列表)
哈希表也称为散列表,用于快速存储和检索数据。通过键映射到存储位置离开视线高效的查找。
基本思想: 利用哈希函数将键映射为数组中的索引。哈希函数是一种将任意大小的数据映射为固定大小值的函数。当我们要存储键值对时,将键通过哈希函数计算出索引,然后在数组中的该索引位置存储对应的值。
示例:
下列键(key)为人名,value为性别。
一般来说,我们可以把键当作数据的标识符,把值当作数据的内容。
基本操作示例:
#include <iostream>
#include <vector>
#include <list>
class HashTable {
private:
int size; // 哈希表的大小
std::vector<std::list<std::pair<int, std::string>>> table; // 哈希表
public:
HashTable(int tableSize) {
size = tableSize;
table.resize(size);
}
int hashFunction(int key) {
return key % size;
}
void insert(int key, std::string value) {
int index = hashFunction(key);
table[index].emplace_back(key, value);
}
std::string search(int key) {
int index = hashFunction(key);
for(auto& pair : table[index]) {
if(pair.first == key) {
return pair.second;
}
}
return "";
}
void remove(int key) {
int index = hashFunction(key);
for(auto it = table[index].begin(); it != table[index].end(); ++it) {
if(it->first == key) {
table[index].erase(it);
break;
}
}
}
};
int main() {
HashTable hashTable(10);
hashTable.insert(1, "Alice");
hashTable.insert(2, "Bob");
hashTable.insert(11, "Charlie");
std::cout << hashTable.search(1) << std::endl; // 输出:Alice
std::cout << hashTable.search(2) << std::endl; // 输出:Bob
std::cout << hashTable.search(11) << std::endl; // 输出:Charlie
hashTable.remove(2);
std::cout << hashTable.search(2) << std::endl; // 输出:(空字符串)
return 0;
}
冲突
冲突指的是两个或多个不同的键经过哈希函数计算得到相同的哈希值,从而导致他们在数组中应存储在同一个位置的情况。
哈希冲突时不可避免的,因为哈希函数将一个大的键空间映射到一个较小的数组索引空间中,所以不同的键可能会映射到相同的位置。
解决方法
-
链地址法:哈希表的每个索引位置都维护一个链表或其他数据结构,当发生冲突时,冲突的键值对将被添加到链表中。这样,每个索引位置可以存储多个键值对,通过遍历链表或其他数据结构来查找或删除特定的键值对。
-
开放地址法:当发生冲突时,采用一定的方法在哈希表汇总寻找另一个可用的位置来存储该键值对。常见的开放地址法有线性探测、二次探测和双重哈希等方法。使用开放地址法时,哈希表中的每个位置只能存储一个键值对,通过一定的探测方法来找到下一个可用的位置。
示例:
当使用链地址法解决哈希冲突时,哈希表的每个索引位置都维护一个链表或其他数据结构,用于存储冲突的键值对。示例代码如下:
#include <iostream>
#include <vector>
#include <list>
class HashTable {
private:
int size;
std::vector<std::list<std::pair<int, std::string>>> table;
public:
HashTable(int tableSize) {
size = tableSize;
table.resize(size);
}
int hashFunction(int key) {
return key % size;
}
void insert(int key, std::string value) {
int index = hashFunction(key);
table[index].emplace_back(key, value);
}
std::string search(int key) {
int index = hashFunction(key);
for(auto& pair : table[index]) {
if(pair.first == key) {
return pair.second;
}
}
return "";
}
void remove(int key) {
int index = hashFunction(key);
for(auto it = table[index].begin(); it != table[index].end(); ++it) {
if(it->first == key) {
table[index].erase(it);
break;
}
}
}
};
int main() {
HashTable hashTable(10);
hashTable.insert(2021001, "Alice");
hashTable.insert(2021002, "Bob");
hashTable.insert(2021003, "Charlie");
std::cout << hashTable.search(2021001) << std::endl; // 输出:Alice
std::cout << hashTable.search(2021002) << std::endl; // 输出:Bob
std::cout << hashTable.search(2021003) << std::endl; // 输出:Charlie
hashTable.remove(2021002);
std::cout << hashTable.search(2021002) << std::endl; // 输出:(空字符串)
return 0;
}
在上述示例中,当发生冲突时,我们使用 std::list
来存储冲突的键值对。每个索引位置都是一个链表,冲突的键值对按照插入的顺序添加到链表中。通过遍历链表,我们可以在哈希表中查找特定的键对应的值。
另一种解决哈希冲突的方法是开放地址法,其中包括线性探测、二次探测和双重哈希等方法。下面是使用线性探测法解决哈希冲突的示例代码:
#include <iostream>
#include <vector>
#include <string>
class HashTable {
private:
int size;
std::vector<std::pair<int, std::string>> table;
std::vector<bool> occupied;
public:
HashTable(int tableSize) {
size = tableSize;
table.resize(size);
occupied.resize(size, false);
}
int hashFunction(int key) {
return key % size;
}
void insert(int key, std::string value) {
int index = hashFunction(key);
while (occupied[index]) {
index = (index + 1) % size; // 线性探测
}
table[index] = std::make_pair(key, value);
occupied[index] = true;
}
std::string search(int key) {
int index = hashFunction(key);
while (occupied[index]) {
if (table[index].first == key) {
return table[index].second;
}
index = (index + 1) % size; // 线性探测
}
return "";
}
void remove(int key) {
int index = hashFunction(key);
while (occupied[index]) {
if (table[index].first == key) {
occupied[index] = false;
return;
}
index = (index + 1) % size; // 线性探测
}
}
};
int main() {
HashTable hashTable(10);
hashTable.insert(2021001, "Alice");
hashTable.insert(2021002, "Bob");
hashTable.insert(2021003, "Charlie");
std::cout << hashTable.search(2021001) << std::endl; //输出:Alice
std::cout << hashTable.search(2021002) << std
std::cout << hashTable.search(2021003) << std::endl; // 输出:Charlie
hashTable.remove(2021002);
std::cout << hashTable.search(2021002) << std::endl; // 输出:(空字符串)
return 0;
}
树
树是一种非线性数据结构,由几点和边组成。树的一个重要特点是他的层次结构,其中一个节点可以有多个子节点,但每个子节点只能有一个父节点
概念组成:
-
节点(Node):树的基本单元,每个节点表示一个元素或数据。每个节点可以有零个或多个子节点,除了根节点外,每个节点都有一个父节点。
-
根节点(Root):树的顶部节点,没有父节点,是树的起点。
-
子节点(Child):节点的直接后继节点,一个节点可以有多个子节点。
-
父节点(Parent):节点的直接前驱节点成为父节点,每个节点都有一个父节点(除了根节点。
-
兄弟节点(Sibling):具有同一个父节点的结点。
-
叶节点(Leaf):没有子节点的结点。
-
子树(Subtree):以某个节点为根节点的子树由该节点及其所有后代节点组成的子树。
特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
二叉树
二叉树是一种特殊的树结构,其中每个节点最多有两个子节点,分别成为左子结点和右子节点。
特点:
-
每个节点最多有两个子节点
-
左子结点的值小于等于父节点的值,右子节点的值大于等于父节点的值
-
子节点顺序不重要,即左子结点没有要求比右子节点小,反之亦然
二叉树遍历
#include <iostream>
using namespace std;
// 二叉树节点的定义
struct TreeNode {
int data; // 节点存储的数据
TreeNode* left; // 左子节点的指针
TreeNode* right; // 右子节点的指针
TreeNode(int d) {
data = d;
left = nullptr;
right = nullptr;
}
};
// 插入节点到二叉树的函数
void insertNode(TreeNode* &root, int value) {
if (root == nullptr) {
// 如果树为空,则创建一个新节点作为根节点
root = new TreeNode(value);
return;
}
if (value < root->data) {
// 如果插入值小于当前节点值,则插入到左子树
insertNode(root->left, value);
} else {
// 如果插入值大于等于当前节点值,则插入到右子树
insertNode(root->right, value);
}
}
// 遍历二叉树的函数 - 前序遍历
void preOrderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 先访问根节点
cout << root->data << " ";
// 递归遍历左子树
preOrderTraversal(root->left);
// 递归遍历右子树
preOrderTraversal(root->right);
}
// 遍历二叉树的函数 - 中序遍历
void inOrderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 递归遍历左子树
inOrderTraversal(root->left);
// 访问根节点
cout << root->data << " ";
// 递归遍历右子树
inOrderTraversal(root->right);
}
// 遍历二叉树的函数 - 后序遍历
void postOrderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 递归遍历左子树
postOrderTraversal(root->left);
// 递归遍历右子树
postOrderTraversal(root->right);
// 访问根节点
cout << root->data << " ";
}
int main() {
TreeNode* root = nullptr; // 根节点初始化为nullptr
// 插入节点到二叉树
insertNode(root, 50);
insertNode(root, 30);
insertNode(root, 20);
insertNode(root, 40);
insertNode(root, 70);
insertNode(root, 60);
insertNode(root, 80);
cout << "前序遍历结果:";
preOrderTraversal(root);
cout << endl;
cout << "中序遍历结果:";
inOrderTraversal(root);
cout << endl;
cout << "后序遍历结果:";
postOrderTraversal(root);
cout << endl;
return 0;
}
堆
堆是一种特殊的数据结构,他是一个完全二叉树(即除最后一层外,其他结点都是满的,最有一层结点从做到右填充)并且满足一下特性:
-
大根堆:每个节点的值都大于等于其子节点
-
小跟对:每个节点的值都小于等于其子节点
以下是用C++示例实现堆的主要操作(以最小堆为例):
#include <iostream>
#include <vector>
#include <algorithm>
// 堆类
class MinHeap {
private:
std::vector<int> heap;
// 获取父节点索引
int parent(int index) {
return (index - 1) / 2;
}
// 获取左子节点索引
int leftChild(int index) {
return 2 * index + 1;
}
// 获取右子节点索引
int rightChild(int index) {
return 2 * index + 2;
}
// 上移操作,用于调整插入操作后的堆
void siftUp(int index) {
while (index > 0 && heap[index] < heap[parent(index)]) {
std::swap(heap[index], heap[parent(index)]);
index = parent(index);
}
}
// 下移操作,用于调整删除操作后的堆
void siftDown(int index) {
int smallest = index;
int l = leftChild(index);
int r = rightChild(index);
if (l < heap.size() && heap[l] < heap[smallest]) {
smallest = l;
}
if (r < heap.size() && heap[r] < heap[smallest]) {
smallest = r;
}
if (smallest != index) {
std::swap(heap[index], heap[smallest]);
siftDown(smallest);
}
}
public:
// 插入元素
void insert(int value) {
heap.push_back(value);
siftUp(heap.size() - 1);
}
// 删除堆顶元素
void removeMin() {
if (heap.empty()) {
return;
}
std::swap(heap[0], heap[heap.size() - 1]);
heap.pop_back();
siftDown(0);
}
// 获取堆顶元素
int getMin() {
if (heap.empty()) {
return -1; // 假设堆中不存负数
}
return heap[0];
}
// 判断堆是否为空
bool isEmpty() {
return heap.empty();
}
};
int main() {
MinHeap heap;
heap.insert(5); // 插入元素5
heap.insert(2); // 插入元素2
heap.insert(8); // 插入元素8
heap.insert(1); // 插入元素1
std::cout << "Min element: " << heap.getMin() << std::endl; // 获取最小元素
heap.removeMin(); // 删除最小元素
std::cout << "Min element: " << heap.getMin() << std::endl;
return 0;
}
这个示例展示了如何使用C++实现最小堆。堆类包括了插入、删除、获取最小元素和判断堆是否为空等主要操作。通过在堆中插入一些元素,并可以获取和删除最小元素。
图
图是一种常见的数据结构,用于表示对象之间的关系。它由一组顶点(节点)和连接这些顶点的边组成。
图可以用来解决一些实际问题,例如网络拓扑、社交网络、路线规划等。在图中,顶点表示实体或对象,而边表示它们之间的关联或连接。
图可以分为有向图和无向图:
-
有向图:有向图中的边有一个方向,从一个顶点指向另一个顶点。这意味着在有向图中,从顶点A到顶点B的路径不一定与从顶点B到顶点A的路径相同。
-
无向图:无向图中的边没有方向,它们可以在顶点之间来回传递。在无向图中,从顶点A到顶点B的路径与从顶点B到顶点A的路径是相同的。
图还可以根据是否允许顶点与自身之间存在边来进行分类:
-
简单图:简单图中不存在自环,即顶点与自身之间没有边。
-
多重图:多重图中允许顶点与自身之间存在多个边,也称为自环。
图可以使用不同的数据结构进行表示,如邻接矩阵和邻接表。
-
邻接矩阵:邻接矩阵是一个二维数组,其中行列表示顶点,数组中的值表示顶点之间的边。如果两个顶点之间存在边,则对应的矩阵元素为1或权重值;否则,为0或不存在边。邻接矩阵适合表示稠密图,但对于稀疏图可能会浪费空间。
-
邻接表:邻接表是由链表组成的数组,数组中的每个元素表示一个顶点,链表包含与该顶点相邻的顶点。邻接表适合表示稀疏图,它节省内存空间,但在查找特定边时需要遍历链表。
图的常见操作包括:
-
增加顶点和边:向图中添加新的顶点和边。
-
删除顶点和边:从图中删除指定的顶点和边。
-
遍历图:访问图中的所有顶点和边,以便查找特定的元素或执行某种操作。
-
搜索路径:在图中查找两个顶点之间的路径,如深度优先搜索(DFS)和广度优先搜索(BFS)等算法。
图的时间复杂度取决于具体的操作和图的实现方式,通常增加、删除顶点或边的时间复杂度为O(1),遍历和搜索路径的时间复杂度取决于图的规模和结构。
总结来说,图是一种用于表示对象之间关系的数据结构,分为有向图和无向图,并可以根据是否允许自环进行分类。图可以使用邻接矩阵或邻接表等数据结构进行表示,提供了增加、删除顶点和边、遍历和搜索路径等操作。