算法是什么
计算=信息处理
借助某种工具,遵照一定规则,以明确而机械的形式进行
计算模型=计算机=信息处理工具
所谓算法,即特定计算模型下,旨在解决特定问题的指令序列
- 输入 待处理的信息(问题)
- 输出 经处理的信息(答案)
- 正确性 的确可以解决指定的问题
- 确定性 任一算法都可以描述为一个由基本操作组成的序列
- 可行性 每一基本操作都可实现,且在常数时间内完成
- 有穷性 对于任何输入,经有穷次基本操作,都可以得到输出
程序等于算法加数据结构
两个主要方面
- 时间复杂度
- 空间复杂度
O、Θ、Ω、o、ω区别
- Θ同时定义了上界和下界,f(n)位于上界和下界之间,且包含等号。
对于f(n),存在正数n0、c1、c2,使得当 n>=n0 时,始终存在 0 <= c1g(n) <= f(n) <= c2g(n),则我们可以用 f(n)=Θ(g(n))表示。
- O定义了算法的上界。
对于f(n),存在正数n0、c,使得当 n>=n0 时,始终存在 0 <= f(n) <= c*g(n),则我们可以用 f(n)=O(g(n))表示。
- o定义的也是算法的上界,不过它不包含等于,是一种不精确的上界,或者称作松上界(某些书籍翻译为非紧上界)。
对于f(n),存在正数n0、c,使得当 n>n0 时,始终存在 0 <= f(n) < c*g(n),则我们可以用 f(n)=o(g(n))表示。
- Ω定义了算法的下界,与O正好相反。
对于f(n),存在正数n0、c,使得当 n>n0 时,始终存在 0 <= f(n) < c*g(n),则我们可以用 f(n)=o(g(n))表示。
- ω同样定义的是下界,只不过不包含等于,是一种不精确的下界,或者称作松下界(某些书籍翻译为非紧下界)。
对于f(n),存在正数n0、c,使得当 n>n0 时,始终存在 0 <= c*g(n) < f(n),则我们可以用 f(n)=ω(g(n))表示。
排序算法
快排(Quick Sort)
void quick_sort(vector<int> &nums, int l, int r) {
if (l + 1 >= r) {
return;
}
int first = l, last = r - 1, key = nums[first];
while (first < last){
while(first < last && nums[last] >= key) {
--last;
}
nums[first] = nums[last];
while (first < last && nums[first] <= key) {
++first;
}
nums[last] = nums[first];
}
nums[first] = key;//把第一个数调到最终位置上
quick_sort(nums, l, first);
quick_sort(nums, first + 1, r);
}
插入排序(Insertion Sort)
void insertion_sort(vector<int> &nums, int n) {
for (int i = 0; i < n; ++i) {
for (int j = i; j > 0 && nums[j] < nums[j-1]; --j) {
swap(nums[j], nums[j-1]);//每次把后面未排序的序列中第一个移到前面已排序的序列中
}
}
}
冒泡排序(Bubble Sort)
void bubble_sort(vector<int> &nums, int n) {
bool swapped;
for (int i = 1; i < n; ++i) {
swapped = false;
for (int j = 1; j < n - i + 1; ++j) {
if (nums[j] < nums[j-1]) {
swap(nums[j], nums[j-1]);//每次把最大的调到最后
swapped = true;
}
}
if (!swapped) {
break;
}
}
}
选择排序(Selection Sort)
void selection_sort(vector<int> &nums, int n) {
int mid;
for (int i = 0; i < n - 1; ++i) {
mid = i;
for (int j = i + 1; j < n; ++j) {
if (nums[j] < nums[mid]) {
mid = j;
}
}
swap(nums[mid], nums[i]);//每次从后面选最小与前面交换
}
}
快排在递归进行部分的排序的时候,只会访问局部的数据,因此缓存能够更大概率的命中;而堆排序的建堆过程是整个数组各个位置都访问到的,后面则是所有未排序数据各个位置都可能访问到的,所以不利于缓存发挥作用。简答的说就是快排的存取模型的局部性(locality)更强,堆排序差一些。
贪心算法
顾名思义,贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。
举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的贪心策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略。
分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
i=j=m=0
while i<len(g) and j<len(s):
if s[j]>=g[i]:
m+=1
i+=1
j+=1
return m
int findContentChildren(vector<int>& children, vector<int>& cookies) {
sort(children.begin(), children.end());
sort(cookies.begin(), cookies.end());
int child = 0, cookie = 0;
while (child < children.size() && cookie < cookies.size()) {
if (children[child] <= cookies[cookie]) ++child;
++cookie;
}
return child;
}
分发糖果
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
def candy(self, ratings: List[int]) -> int:
l=len(ratings)
a=[1 for i in range(l)]
for i in range(1,l):
if ratings[i-1]<ratings[i]:
a[i]=a[i-1]+1
for i in range(l-1,0,-1):
if ratings[i]<ratings[i-1]:
a[i-1]=max(a[i-1],a[i]+1)
return sum(a)
int candy(vector<int>& ratings) {
int size = ratings.size();
if (size < 2) {
return size;
}
vector<int> num(size, 1);
for (int i = 1; i < size; ++i) {
if (ratings[i] > ratings[i-1]) {
num[i] = num[i-1] + 1;
}
}
for (int i = size - 1; i > 0; --i) {
if (ratings[i] < ratings[i-1]) {
num[i-1] = max(num[i-1], num[i] + 1);
}
}
return accumulate(num.begin(), num.end(), 0);
}
把所有孩子的糖果数初始化为 1;先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。
无重叠区间
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
a=sorted(intervals, key=lambda i: i[1])
b,s=a[0][1],0
for i in a[1:]:
if b>i[0]:
s+=1
else:
b=i[1]
return s
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.empty()) {
return 0;
}
int n = intervals.size();
sort(intervals.begin(), intervals.end(), [](vector<int> a, vector<int> b) {
return a[1] < b[1];
});
int total = 0, prev = intervals[0][1];
for (int i = 1; i < n; ++i) {
if (intervals[i][0] < prev) {
++total;
} else {
prev = intervals[i][1];
}
}
return total;
}
在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为,优先保留结尾小且不相交的区间。具体实现方法为,先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。