基本排序算法
冒泡排序
没什么可说的, 改进方法就是加一个标志位防止有序后重复遍历. 由于需要遍历两次, 所以时间复杂度O(N^2)
- 传送门 --> 冒泡排序
选择排序
外层从0开始默认outer是最小数的下标, 内存从outer+1位置开始遍历, 不稳定, 如{ 3, 3, 3, 2 }, 当比较最后一个4时, 是第一个3和2交换, 从而不稳定. 内外层遍历两次, 时间复杂度O(N^2)
- 传送门 --> 选择排序
插入排序
相当于打扑克排序, outer从1到N-1, inner从outer到N-1, 时间复杂度O(N^2)
插入排序选择排序冒泡排序有浪费许多比较的次数
归并排序快的是因为小范围合并为大范围时, 有序可以同过外排方式
小组和为大组时, 组内有序没有浪费, 永远是组与组之间的比较
- 传送门 --> 插入排序
希尔排序
- 传输门 --> 希尔排序
归并排序
递归把一个数字分隔为两部分, T(N) = 2*T(N/2) + O(N)
, a= 2, b = 2, N = 1, 时间复杂度O(N*logN)
, 额外空间复杂度O(N)
递归符合master公式: T(N) = a*T(N/b) + O(N^d)
时间复杂度为:
(1) log(b, a) > d --> 复杂度O(N^(log(b, a)))
(2) log(b, a) = d --> 复杂度O(N^d * logN)
(3) log(b, a) < d --> 复杂度O(N^d)
- 传送门 --> 归并排序
快排
递归公式同归并排序, 由于需要记录分隔点, 所以额外空间复杂度O(logN), 快排做不到稳定性, 因为partition过程做不到稳定
- 经典快排与数据状况有关, 这是因为分隔点选取的问题, 如{1, 2, 3, 4, 5, 6}分隔点选取最右边时每次只排序一个数字, 此时时间复杂度为O(N^2)
- 如果分隔点选取中位数, 则每次恰好可把数组划分为两部分, 时间复杂度为O(N*logN)
- 随机快排的分隔点随机选取, 把复杂度转化为与概率有关, 复杂度长期期望为O(N*logN)
- 传输门 --> 快排
堆排
大根堆结构重要两个函数heapInsert与heapify, 一个上浮, 一个是下沉, 优先级队列就是堆
建立堆的时间复杂度为O(log1) + O(log2) + O(log3) + ... + O(log(N)), 收敛域O(N)
- 算法: 插入时, 上浮, 直至没有父节点比当前节点大; 排序交换堆顶与堆未元素, 这时堆顶元素下沉, 直至当前节点比子节点都大
- 传送门 --> 堆排
排序补充
- 归并排序可以做到额外空间复杂度O(1), 有难度, 相关搜索"归并排序内部缓存法"
- 快排可以做到稳定性, 有难度, 不要求掌握, 相关搜索"01 stable sort"; 快排的优势是partition过程中, 时间复杂度O(N), 空间复杂度O(1)
- 有一道题目, 奇数放在数组的左边边, 偶数放在数组右边, 要求原始的相对次数不变, 牛客练习 --> 调整数组顺序使奇数位于偶数前面
排序总结
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N*logN) ~ O(N^2) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
快排 | O(N*logN) | O(N * logN) | O(N^2) | O(logN) ~ O(N) | 不稳定 |
工程中的综合排序算法
基础类型很长时, 使用快排, 因为基础数据类型不要求稳定
复合数据类型长度很长时, 使用归并排序, 复合数据类型要求稳定
任何数据类型的数组长度很短(<60)时, 使用插入排序
桶排序
桶排序是一个逻辑概念, 具体实现方法是计算排序, 基数排序
- 非基于比较的排序, 与被排序的样本的实际数据状况有很大关系, 所以实际中并不经常使用
- 时间复杂度O(N), 额外空间复杂度O(N)
- 稳定
计算排序
基数排序
一个有序数组A, 另一个无序数组B, 打印B中所有不在A中的数组, A数组长度为N, B数组长度为M
- 算法1: 暴力法, 时间复杂度O(N^2)
- 算法2: 遍历B数组, 使用二分法在A中查找相同元素, 时间复杂度
O(M * logN)
- 算法3: 把A数组进行排序, 排序最小时间复杂度
O(M*logM)
, 使用类似外排进行排序, 总时间复杂度O(M*logM) + O(M+N)
两个游标, p1指向A, p2指向B
(1) A[p1] < B[p2]; p1++
(2) A[p1] == B[p2]; p1不移动, 移动p2, 因为B数组中可能有重复数字所以只移动p2
(3) A[p1] > B[p2]; 打印并移动p2 - 传送门 --> 时间复杂度理解
使用递归查找数组中的最大值
- 算法: 二分法查找分隔数字, 返回左右数组中的最大值
本算法中T(N) = 2*T(N/2) + O(1)
, a=b=2, d=0, log(2, 2)=1 > d=0, 复杂度为O(N^log(2, 2))=O(N) - 传送门 --> 使用递归查找最大值
小和
在一个数组中, 每一个数左边比当前数小的数累加起来, 叫做这个数组的小和. 求一个数字的小和. 如[1, 3, 4, 2, 5]小和为16
- 算法: 利用归并排序, 关键步骤
res += (arr[lowPtr] < arr[hightPtr] ? arr[lowPtr] * (right - hightPtr + 1) : 0);
其中arr[lowPtr] * (right - hightPtr + 1)
是关键, 和归并排序一样, 没有浪费之前的比较次数
本题递归公式T(N) = 2*T(N/2) + O(N)
, 时间复杂度同归并排序, 同位O(N*logN) - 传送门 --> 小和
逆序对
在一个数组中, 左边的数如果比右边的数大, 则这两个数构成一个逆序对, 请打印所有逆序对
- 算法: 与小和相同, 只是在merge时把数组从大到小排列, 关键步骤
res += (vt[leftPtr] > vt[rightPtr] ? (right - rightPtr + 1) : 0);
与上述相差大小号和乘一个数区别, 目前只能统计个数, 打印有些问题 - 传送门 --> 逆序对
- 牛客练习 --> 数组中的逆序对
荷兰国旗问题
给定一个数组arr, 和一个数num, 请把小于num的数放在数组的左边, 等于num的数放在数组的中间, 大于num的数放在数组的右边, 要求额外空间复杂度O(1), 时间复杂度O(N)
- 算法: 准备三个游标leftPtr初始指向数组边界起始位置前一个元素即left-1, rightPtr初始指向末尾后一个元素right+1, index从头到尾遍历数组比较
(1) 比num小, index指向元素和leftPtr指向的下一元素交换, index和leftPtr同时+1
(2) 等于num, 只把index+1
(3) 大于num, 把index元素和rightPtr指向的元素前一个交换, rightPtr-1, 由于不确定rightPtr指向元素和num的关系, index不变 - 传输门 --> 荷兰国旗问题
数据流的中位数
实质利用堆排序找中位数
- 算法: 实质是利用堆
- 牛客练习 --> 数据流中的中位数
排序数组最大差值
给定一个数组, 求排序之后相邻两数的最大差值, 要求时间复杂度O(N), 且要求使用非基于比较的排序
- 算法: 运用桶的概念. N个数, 准备N+1个桶, 最小值放0号桶, 最大桶放N号桶放N号桶;
三个数组, 分别记录桶是否有值, 桶内最大值, 桶内最小值; 相邻两个数最大差值可能存在于两个非空桶之间, 也可能存在于空桶之间 - 传输门 --> 排序数组最大差值
数据流中位数
如何可以得到数据流中排序后的中位数
用数组结构实现大小固定栈
- 传输门 --> 数组实现大小固定的栈
用数组结构实现大小固定队列
- 算法: 使用三个变量: start, end, size. 目的是为了是start和end解耦合, 使start只与size有关, end只与size有关.
插入只与m_size和m_arraySize有关, 弹出只与m_size和0有关; 弹出直接弹出m_start位置的元素, 插入直接插入到m_end位置
m_start的变化只与m_arraySize有关; m_end的变化只与m_arraySize有关. m_start与m_end都是+1递增 - 传输门 --> 数组实现大小固定的队列
仅用队列结构实现栈结构
传送门 -->
仅用栈结构实现队列结构
双栈返回最小值
实现一个特殊的栈, 在实现栈的基本功能的基础上, 再实现返回栈中的最小元素的操作. 要求: 1. pop, push, getMin操作的时间复杂度都是O(1); 2. 设计的栈类型可以使用现成的栈结构
顺时针打印矩阵
给定一个整型矩阵matrix, 按照从外向里以顺时针的顺序依次打印出每一个数字. 例如:如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
旋转正方形
给定一个整型正方形矩阵matrix, 把该矩阵调整成顺时针转换90度的样子. 额外空间复杂度O(1)
- 算法: 同顺时针打印矩阵一样, 抽象一个顺时针打印边框的函数, 从外到内依次调用这个函数.
- 传送门 --> 旋转正方形矩阵
之字形打印矩阵
给定一个矩阵matrix, 按照"之"字形方式打印这个矩阵
- 算法: 抽象一个打印斜行的矩阵, 通过一个布尔变量来判断从上到下还是从下到上, 调用函数多了些边界判断. x_1和y_1, x_2和y_2先后移动次序有很大关系, 先比较x_1再判断y_1, 先比较y_2再判断x_2,
- 传输门 --> 之字形打印矩阵
行列有序的矩阵中查找
给定一个由N*M的整数型矩阵matrix和一个整数K, matrix的每一行和每一列都是排好序的. 实现一个函数, 判断K是否在matrix中. 要求时间复杂度O(N+M), 额外空间复杂度O(1)
- 算法: 从矩阵的特性出发, 确定查找方法. 设置查找起始点设置为右上角
若K小于右上角的数, 则不可能在当前列的下面, 左移起始点
若K大于右上角的数, 则不可能在当前行的左边, 下移起始点 - 传送门 --> 行列有序的矩阵中查找
- 牛客练习 --> 二维数组中的查找
链表面试与笔试
链表问题往往在空间复杂度上下工夫, 时间复杂度基本是O(N)或O(N^2)
笔试: 目的最快把问题过掉, 不追求空间复杂度
面试: 和面试官聊空间复杂度
分别实现反转单向链表和反转双向链表函数
- 算法: 分别定义两个指针, next和pre, 当head不为空时, next和pre都初始化为nullptr
(1)next指向head下一节点
(2)利用pre与head反转单/双链表
(3)使pre指向head
(4)使head指向next - 递归反转单链表:
(1)递归到最后一个节点, 然后依次返回
(2)当前节点的下一节点的next指向当前节点
(3)当前节点的next域指向nullptr - 传送门 --> 反转单双链表
- 牛客练习 --> 反转链表
判断一个链表是否是回文结构
给定一个链表的头结点head, 判断该链表是否是会问结构. 如: 1, 2, 1返回true; 1, 2, 2, 1返回true; 1, 2, 3返回false. 如果求时间复杂度O(N), 额外空间复杂度O(1)
- 算法1: 使用栈把元素逆序, 遍历两次列表, 第一次压栈, 第二次和栈中元素对比是否相同, 时间复杂度O(N), 空间复杂度O(N)
- 算法2: 使用快慢指针
单向链表划分
将单向链表按某值划分成左边小, 中间相等, 右边大的形式
复制含有随机指针节点的链表
两个连续相交问题
给定三角形ABC和一点P(x,y,z),判断点P是否在ABC内,给出思路并手写代码
- 面积计算公式及浮点数的比较
#include <iostream>
#include <math.h>
using namespace std;
#define ABS_FLOAT_0 0.0001 // 浮点数比较
struct point_float {
float x;
float y;
};
float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2) {
point_float AB, BC;
AB.x = pt1.x - pt0.x;
AB.y = pt1.y - pt0.y;
BC.x = pt2.x - pt1.x;
BC.y = pt2.y - pt1.y;
return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
}
bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D){
float SABC, SADB, SBDC, SADC;
SABC = GetTriangleSquar(A, B, C);
SADB = GetTriangleSquar(A, D, B);
SBDC = GetTriangleSquar(B, D, C);
SADC = GetTriangleSquar(A, D, C);
float SumSuqar = SADB + SBDC + SADC;
if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0))
return true;
else
return false;
}
n个整数的无序数组,找到每个元素后面比它大的第一个数,要求时间复杂度为O(N)
vector<int> findMax(vector<int>num)
{
if(num.size()==0)return num;
vector<int>res(num.size());
int i=0;
stack<int>s;
while(i<num.size())
{
if(s.empty()||num[s.top()]>=num[i])
{
s.push(i++);
}
else
{
res[s.top()]=num[i];
s.pop();
}
}
while(!s.empty())
{
res[s.top()]=INT_MAX;
s.pop();
}
for(int i=0; i<res.size(); i++)
cout<<res[i]<<endl;
return res;
}