常见算法和数据结构总结
一、基本算法思想
1.贪心算法
将一个大问题分成若干个子问题,求子问题最优解,然后把子问题的解合并成原来问题的解。
例题:小明手中有 1,5,10,50,100 五种面额的纸币,每种纸币对应张数分别为 5,2,2,3,5 张。若小明需要支付 456 元,则需要多少张纸币?
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5;
int Count[N] = { 5,2,2,3,5 };//每一张纸币的数量
int Value[N] = { 1,5,10,50,100 };
int solve(int money) {
int num = 0;
for (int i = N - 1; i >= 0; i--) {
int c = min(money / Value[i], Count[i]);//每一个所需要的张数
money = money - c*Value[i];
num += c;//总张数
}
if (money>0) num = -1;
return num;
}
int main()
{
cout << solve(456) << endl;
system("pause");
return 0;
}
2.动态规划
是一种空间换取时间的算法,一句话解释就是记住你之前得到的答案。
例题:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
#include <iostream>
#include <algorithm>
using namespace std;
int F(int n)
{
if (n == 1)
return 1;
if (n == 2)
return 2;
if (n > 2)
return F(n - 1) + F(n - 2);
}
int climbStairs_F(int n) {
return F(n);
}
int main()
{
cout << climbStairs_F(10) << endl;
system("pause");
return 0;
}
3.分治
分治算法,根据字面意思解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
案例:二分查找,快速排序,归并排序
//二分查找代码
#include <iostream>
using namespace std;
int binarySearch(int array[], int n, int target)
{
int low = 0, high = n, middle = 0;
while (low < high)
{
middle = (low + high) / 2;
if (target == array[middle])
{
return middle;
}
else if (target > array[middle])
{
low = middle + 1;
}
else if (target < array[middle])
{
high = middle;
}
}
return -1;
}
int main()
{
int array[10] = { 0,1,2,3,4,5,6,7,8,9 };
int r = binarySearch(array, 10, 3);
cout << r << endl;
system("pause");
return 0;
}
二、排序算法
4.冒泡排序
冒泡排序是通过比较两个相邻元素的大小实现排序,如果前一个元素大于后一个元素,就交换这两个元素。这样就会让每一趟冒泡都能找到最大一个元素并放到最后。
冒泡排序代码
#include<iostream>
using namespace std;
//冒泡排序
int main()
{
int a[9] = { 1,9,2,5,8,3,7,4,6 };
int i, j;
for (i = 0; i<9; i++)
{
for (j = 8; j>i; j--)
{
if (a[j - 1] > a[j])
{
int temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
}
}
}
for (int i = 0; i<9; i++)
cout << a[i] << " ";
system("pause");
return 0;
}
5.选择排序
选择排序的思想是,依次从「无序列表」中找到一个最小的元素放到「有序列表」的最后面。以 arr = [ 8, 1, 4, 6, 2, 3, 5, 4 ] 为例,排序开始时把 arr 分为有序列表 A = [ ], 无序列表 B = [ 8, 1, 4, 6, 2, 3, 5, 4 ],依次从 B 中找出最小的元素放到 A 的最后面。这种排序也是逻辑上的分组,实际上不会创建 A 和 B,只是用下标来标记 A 和 B。
以 arr = [ 8, 1, 4, 6, 2, 3, 5, 4 ] 为例,第一次找到最小元素 1 与 8 进行交换,这时有列表 A = [1], 无序列表 B = [8, 4, 6, 2, 3, 5, 4];第二次从 B 中找到最小元素 2,与 B 中的第一个元素进行交换,交换后 A = [1,2],B = [4, 6, 8, 3, 5, 4];就这样不断缩短 B,扩大 A,最终达到有序。
选择排序代码
#include <iostream>
#include <algorithm>
using namespace std;
void selectionSort(int arr[], int n) {
for (int i = 0; i < n; i++) {
// 寻找[i, n)区间里的最小值
int minIndex = i;
for (int j = i + 1; j < n; j++)
if (arr[j] < arr[minIndex])
minIndex = j;
swap(arr[i], arr[minIndex]);
}
}
int main() {
int a[10] = { 8,1,4,6,2,3,5,4 };
selectionSort(a, 8);
for (int i = 0; i < 8; i++)
cout << a[i] << " ";
cout << endl;
system("pause");
return 0;
}
6.插入排序
在整个排序过程中,以 arr = [ 8, 1, 4, 6, 2, 3, 5, 7] 为例,它会把 arr 分成两组 A = [ 8 ] 和 B = [ 1, 4, 6, 2, 3, 5, 7] ,逐步遍历 B 中元素插入到 A 中,最终构成一个有序序列。
插入排序代码
//插入排序
#include <cstdio>
#include<iostream>
using namespace std;
void InsertSort(int a[], int n)
{
for (int j = 1; j < n; j++)
{
int key = a[j]; //待排序第一个元素
int i = j - 1; //代表已经排过序的元素最后一个索引数
while (i >= 0 && key < a[i])
{
//从后向前逐个比较已经排序过数组,如果比它小,则把后者用前者代替,
//其实说白了就是数组逐个后移动一位,为找到合适的位置时候便于Key的插入
a[i + 1] = a[i];
i--;
}
a[i + 1] = key;//找到合适的位置了,赋值,在i索引的后面设置key值。
}
}
int main() {
int d[] = { 8, 1, 4, 6, 2, 3, 5, 7 };
cout << "输入数组 { 8, 1, 4, 6, 2, 3, 5, 7 } " << endl;
InsertSort(d, 8);
cout << "排序后结果:";
for (int i = 0; i < 8; i++)
{
cout << d[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
7.希尔排序
它的核心思想是把一个序列分组,对分组后的内容进行插入排序,这里的分组只是逻辑上的分组,不会重新开辟存储空间。它其实是插入排序的优化版,插入排序对基本有序的序列性能好,希尔排序利用这一特性把原序列分组,对每个分组进行排序,逐步完成排序。
希尔排序代码
//希尔排序
#include <iostream>
#include <stdlib.h>
using namespace std;
void ShellSort(int array[], int n) //希尔排序函数
{
int i, j, step;
for (step = n / 2; step > 0; step = step / 2)
{
for (i = 0; i < step; i++) //i是子数组的编号
{
for (j = i + step; j < n; j = j + step) //数组下标j,数组步长下标j+step
{
if (array[j] < array[j - step])
{
int temp = array[j]; //把数组下标j的值放到temp中
int k = j - step;
while (k >= 0 && temp < array[k])
{
array[k + step] = array[k]; //把大的值往后插入
k = k - step;
}
array[k + step] = temp; //把小的值往前插入
}
}
}
}
}
int main(void) //主程序
{
int array[] = { 8, 1, 4, 6, 2, 3, 5, 7 };
ShellSort(array, 8); // 调用BubbleSort函数 进行比较
cout << "由小到大的顺序排列后:" << endl;
for (int i = 0; i < 8; i++)
{
cout << array[i] << " ";
}
cout << endl << endl; //换行
system("pause"); //调试时,黑窗口不会闪退,一直保持
return 0;
}
8.快速排序
快速排序的核心思想是对待排序序列通过一个「支点」(支点就是序列中的一个元素,别把它想的太高大上)进行拆分,使得左边的数据小于支点,右边的数据大于支点。然后把左边和右边再做一次递归,直到递归结束。
快速排序代码
#include <iostream>
#include <vector>
using namespace std;
//快速排序(从小到大)
void quickSort(int left, int right, int arr[])
{
if (left >= right)
return;
int i, j, base, temp;
i = left, j = right;
base = arr[left]; //取最左边的数为基准数
while (i < j)
{
while (arr[j] >= base && i < j)
j--;
while (arr[i] <= base && i < j)
i++;
if (i < j)
{
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
//基准数归位
arr[left] = arr[i];
arr[i] = base;
quickSort(left, i - 1, arr);//递归左边
quickSort(i + 1, right, arr);//递归右边
}
int main()
{
int arr[8] = { 8, 1, 4, 6, 2, 3, 5, 7 };
quickSort(0, 7, arr);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
9.归并排序
归并排序,采用分治思想,先把待排序序列拆分成一个个子序列,直到子序列只有一个元素,停止拆分,然后对每个子序列进行边排序边合并。其实,从名字「归并」可以看出一丝「拆、合」的意思。
归并排序代码
//c++归并排序
#include<iostream>
using namespace std;
void Merge(int arr[], int l, int q, int r) {
int n = r - l + 1;//临时数组存合并后的有序序列
int* tmp = new int[n];
int i = 0;
int left = l;
int right = q + 1;
while (left <= q && right <= r)
tmp[i++] = arr[left] <= arr[right] ? arr[left++] : arr[right++];
while (left <= q)
tmp[i++] = arr[left++];
while (right <= r)
tmp[i++] = arr[right++];
for (int j = 0; j<n; ++j)
arr[l + j] = tmp[j];
delete[] tmp;//删掉堆区的内存
}
void MergeSort(int arr[], int l, int r) {
if (l == r)
return; //递归基是让数组中的每个数单独成为长度为1的区间
int q = (l + r) / 2;
MergeSort(arr, l, q);
MergeSort(arr, q + 1, r);
Merge(arr, l, q, r);
}
int main() {
int a[8] = { 8, 1, 4, 6, 2, 3, 5, 7 };
MergeSort(a, 0, 7);
for (int i = 0; i<8; ++i)
cout << a[i] << " ";
cout << endl;
system("pause");
return 0;
}
10.计数排序
计数排序的核心思想是把一个无序序列 A 转换成另一个有序序列 B,从 B 中逐个“取出”所有元素,取出的元素即为有序序列。
计数排序代码
//计数排序
#include <iostream>
#include <vector>
using namespace std;
void CountSort(vector<int> &arr, int maxVal) {
int len = arr.size();
if (len < 1)
return;
vector<int> count(maxVal + 1, 0);
vector<int> tmp(arr);
for (auto x : arr)
count[x]++;
for (int i = 1; i <= maxVal; ++i)
count[i] += count[i - 1];
for (int i = len - 1; i >= 0; --i) {
arr[count[tmp[i]] - 1] = tmp[i];
count[tmp[i]]--; //注意这里要减1
}
}
int main()
{
vector<int> arr = { 8, 1, 4, 6, 2, 3, 5, 4 };
int maxVal = 8;
CountSort(arr, maxVal);
for (auto x : arr)
cout << x << " ";
cout << endl;
system("pause");
return 0;
}
11.桶排序
设置固定数量的空桶。
把数据放到对应的桶中。
对每个不为空的桶中数据进行排序。
拼接不为空的桶中数据,得到结果
桶排序代码
/*算法:桶排序*/
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void bucket_sort(int a[], int n, int max)
{
int i, j;
int *buckets;
if (a == NULL || n<1 || max<1)
return;
// 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。
if ((buckets = (int *)malloc(max * sizeof(int))) == NULL)
return;
memset(buckets, 0, max * sizeof(int));
// 1. 计数
for (i = 0; i < n; i++)
buckets[a[i]]++;
// 2. 排序
for (i = 0, j = 0; i < max; i++)
while ((buckets[i]--) >0)
a[j++] = i;
free(buckets);
}
int main() {
int A[] = { 8, 1, 4, 6, 2, 3, 5, 7 };
bucket_sort(A, 8, 9);
for (int i = 0; i<8; i++)
cout << A[i] << " ";
cout << endl;
system("pause");
return 0;
}
12.基数排序
基数排序是从待排序序列找出可以作为排序的「关键字」,按照「关键字」进行多次排序,最终得到有序序列。
基数排序
#include <iostream>
using namespace std;
/*
* 打印数组
*/
void printArray(int array[], int length)
{
for (int i = 0; i < length; ++i)
{
cout << array[i] << " ";
}
cout << endl;
}
/*
*求数据的最大位数,决定排序次数
*/
int maxbit(int data[], int n)
{
int d = 1; //保存最大的位数
int p = 10;
for (int i = 0; i < n; ++i)
{
while (data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int tmp[10];
int count[10]; //计数器
int i, j, k;
int radix = 1;
for (i = 1; i <= d; i++) //进行d次排序
{
for (j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for (j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for (j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for (j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
}
int main()
{
int array[10] = { 73,22,93,43,55,14,28,65,39,81 };
radixsort(array, 10);
printArray(array, 10);
system("pause");
return 0;
}
三、搜索
1.二叉树的层次遍历
层次遍历很好理解,就是按二叉树从上到下,从左到右依次打印每个节点存储的数据。这不就是队列的拿手绝活么,先进先出。从左到右依次将每个数放入到队列中,然后按顺序依次打印就是想要的结果。
若根节点为空,直接返回; 若根节点非空,则将根节点入队,然后判断其左右子节点是否为空,若不为空,则压入队列。此时将根结点打印输出,并将根结点出队列。依次循环执行,直到队列为空。
二叉树的层次遍历代码
#include<iostream>
#include<queue>
using namespace std;
struct BinaryTreeNode{
int value;
BinaryTreeNode *m_pLeft;
BinaryTreeNode *m_pRight;
};
void addBTNode(BinaryTreeNode **myBT,int val);//添加节点,满足每个父亲节点大于左边的,小于右边的
void levels_showBT(BinaryTreeNode *myBT);//层次遍历,利用队列实现
void levels_showBT_2(BinaryTreeNode *myBT);//层次遍历,利用队列实现
int main(){
BinaryTreeNode *myBT = nullptr;
addBTNode(&myBT,10);
addBTNode(&myBT,2);
addBTNode(&myBT,3);
addBTNode(&myBT,15);
addBTNode(&myBT,18);
addBTNode(&myBT,1);
addBTNode(&myBT,16);
levels_showBT(myBT);
levels_showBT_2(myBT);
return 0;
}
void addBTNode(BinaryTreeNode **myBT,int val){
if(*myBT == nullptr){
*myBT = new BinaryTreeNode();
(*myBT)->value = val;
(*myBT)->m_pLeft = nullptr;
(*myBT)->m_pRight = nullptr;
return;
}
if(val == (*myBT)->value){
return;
}
else if(val < (*myBT)->value){
addBTNode(&(*myBT)->m_pLeft,val);
}
else{
addBTNode(&(*myBT)->m_pRight,val);
}
}
void levels_showBT(BinaryTreeNode *myBT){//层次遍历,利用队列实现
if(myBT == nullptr )
return;
queue<BinaryTreeNode *> que;//构造一个树结点指针的队列
que.push(myBT);
while(!que.empty()){
BinaryTreeNode *q = que.front();
cout<<q->value<<" ";
que.pop();
if( q->m_pLeft != nullptr)//que.front()拿到最前结点
{
que.push( q->m_pLeft );
}
if( q->m_pRight != nullptr){
que.push( q->m_pRight );
}
}
cout<<endl;
}
void levels_showBT_2(BinaryTreeNode *myBT){//层次遍历,利用队列实现
if(myBT == nullptr )
return;
queue<BinaryTreeNode *> que;//构造一个树结点指针的队列
que.push(myBT);
while(!que.empty()){
if( (que.front())->m_pLeft != nullptr)//que.front()拿到最前结点
{
que.push( (que.front())->m_pLeft );
}
if( (que.front())->m_pRight != nullptr){
que.push( (que.front())->m_pRight );
}
cout<<(que.front())->value<<" ";
que.pop();
}
cout<<endl;
}
四、 线性表
数组
1.二维数组的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
二维数组的查找代码
class Solution {
public:
bool Find(int target, vector<vector<int> > array) {
// 判断数组是否为空
int m = array.size();
if (m == 0) return false;
int n = array[0].size();
if (n == 0) return false;
int r = 0, c = n-1; // 右上角元素
while (r<m && c>=0) {
if (target == array[r][c]) {
return true;
}
else if (target > array[r][c]) {
++r;
}
else {
--c;
}
}
return false;
}
};
链表
1.从链表的末尾添加节点
指针的指针参考
从链表的末尾添加节点参考
从链表的末尾添加节点代码
/*
* C++ 用指针的引用,好多了! 考虑得更简单。
*/
#include <iostream>
#include <cstdlib>
#include <cstring>
#define BUG cout << "here\n";
#define STOP system("pause");
using namespace std;
struct node {
int value;
node* next;
node() {
value = 0;
next = NULL;
}
};
void addToTail(node* &phead, int value) {
node* pn = new node();
pn->value = value;
if(phead == NULL) { // 考虑要全面。
phead = pn;
}
else {
node* p = phead;
while(p->next != NULL) {
p = p->next;
}
p->next = pn;
}
}
void addToTail(node** phead, int value) { // 之所以用指向指针的指针 -- 理解 复杂化的简单对比思想来理解
node* pn = (node*)malloc(sizeof(node));
pn->next = NULL;
pn->value = value;
if(*phead == NULL) { // 考虑要全面。
*phead = pn;
}
else {
node* p = *phead;
while(p->next != NULL) {
p = p->next;
}
p->next = pn;
}
}
int main() {
node* head = NULL;
addToTail(&head, 10);
addToTail(&head, 20);
node* p = head;
while(p != NULL) {
printf("%d->", p->value);
p = p->next;
}
printf("NULL");
STOP
return 0;
}
2.从头到尾打印链表
输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)
解题思路
3.反转链表
把一个单链表反转
解题思路
4.链表倒数第k个节点
输入一个链表,输出该链表中倒数第k个结点。
解题思路
5.合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
解题思路
6.两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点。
解题思路
字符串
1.替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"
解题思路
2.字符串的排列
输入一个字符串,按字典序打印出该字符串中字符的所有排列
解题思路
栈和队列
1.用两个栈实现队列
用两个栈实现一个队列
解题思路
2.从上往下打印二叉树
不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印
解题思路
树
1.重建二叉树
给出先序和中序,重建二叉树,以层序输出
解题思路
2.树的子结构
输入两棵二叉树A,B,判断B是不是A的子结构输入两棵二叉树A,B,判断B是不是A的子结构
解题思路
3.二叉树的后续遍历序列
给定一个二叉树,返回他的后序遍历的序列
解题思路
4.二叉树的深度
求二叉树的深度
解题思路
其他
设计LRU缓存结构
设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
set(key, value):将记录(key, value)插入该结构
get(key):返回key对应的value值
解题思路
设计LRU缓存结构代码
class Solution {
public:
/**
* lfu design
* @param operators int整型vector<vector<>> ops
* @param k int整型 the k
* @return int整型vector
*/
// 存频率和频率对应节点链表的字典
unordered_map<int, list<vector<int>>> freq_mp;
// 存对应键值和节点
unordered_map<int, list<vector<int>>::iterator> mp;
// 记录最小频率 用来使用LFU策略找节点
int min_freq=0;
// 存剩余容量
int size=0;
vector<int> LFU(vector<vector<int>>& operators, int k){
vector<int> res;
size=k;
for(int i=0;i<operators.size();++i){
if(operators[i][0]==1){
set(operators[i][1], operators[i][2]);
}else{
res.push_back(get(operators[i][1]));
}
}
return res;
}
void update(list<vector<int>>::iterator iter, int key, int value){
//取出双向链表中的一个节点 即 vector<int>
int freq=(*iter)[0];
freq_mp[freq].erase(iter);
//如果该频率中已经没有节点
//删除频率双向链表中的对应链表
//查看是否需要更新最小频率
if(freq_mp[freq].empty()){
freq_mp.erase(freq);
if(freq == min_freq){
min_freq++;
}
}
//向freq+1的双向链表头部中插入
//由于原来的节点已经不存在 需要重新设置<key,value>的存储
freq_mp[freq+1].push_front({freq+1, key, value});
mp[key]=freq_mp[freq+1].begin();
}
void set(int key, int value){
auto it=mp.find(key);
if(it!=mp.end()){
//链表中存在节点 更新value和频率
update(it->second, key, value);
}else{
//哈希表中没有该节点
//如果链表已满 删除频率最低而且最早的删掉
//频率表中删除最后一个节点
//删除对应<key,value>的节点
if(size==0){
int oldkey=freq_mp[min_freq].back()[1];
freq_mp[min_freq].pop_back();
if(freq_mp[min_freq].empty()){
freq_mp.erase(min_freq);
}
mp.erase(oldkey);
}else{
//容量未满 可以直接加入
size--;
}
min_freq=1;
freq_mp[1].push_front({1, key, value});
mp[key]=freq_mp[1].begin();
}
}
//get操作:没有找到则返回-1
//找到则更新频率并且取出value返回
int get(int key){
int res=-1;
auto it = mp.find(key);
if(it==mp.end()){
return res;
}
auto iter = it->second;
res=(*iter)[2];
update(iter, key, res);
return res;
}
};