今天是我第一次线上面试,是与湖南大学的一名教授进行线上面试,刚开始还是很紧张的,我先是中英文做了一下自我介绍,然后跟老师谈了一下自己的项目经历,然后就是老师问我的一些基础知识,我确实没有准备太好,基础知识都忘记了,在这里做一下自我检讨,下面是导师问我的一些基础知识:
1.直接插入排序
插入排序将数列划分为“已排序的”和“未排序的”两部分,每次从“未排序的”元素中选择一个插入到“已排序的”元素中的正确位置。
插入排序的最坏情况时间复杂度和平均情况时间复杂度都为 O(n^2) ,但其在数列几乎有序时效率很高。
插入排序是一个稳定排序。
C++ 代码:
void insertion_sort(int* a, int n) {
//对 a[1],a[2],...,a[n] 进行插入排序
for (int i = 2; i <= n; ++i) {
int key = a[i];
int j = i - 1;
while (j > 0 && a[j] > key) {
a[j + 1] = a[j];
--j;
}
a[j + 1] = key;
}
}
2.堆排序
堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
ok,了解了这些定义。接下来,我们来看看堆排序的基本思想及基本步骤:
堆排序基本思想及步骤
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构如下
2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
代码实现:
package sortdemo;
import java.util.Arrays;
/*
* 堆排序demo
*/
public class HeapSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
3.贪心算法
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
思想
贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止
过程
建立数学模型来描述问题;
把求解的问题分成若干个子问题;
对每一子问题求解,得到子问题的局部最优解;
把子问题的解局部最优解合成原来解问题的一个解。
例子
假设山洞中有 n 种宝物,每种宝物有一定重量 w 和相应的价值 v,毛驴运载能力有限,
只能运走 m 重量的宝物,一种宝物只能拿一样,宝物可以分割。那么怎么才能使毛驴运走宝物的价值最大呢?
我们可以尝试贪心策略:
(1)每次挑选价值最大的宝物装入背包,得到的结果是否最优?
(2)每次挑选重量最小的宝物装入,能否得到最优解?
(3)每次选取单位重量价值最大的宝物,能否使价值最高?
思考一下,如果选价值最大的宝物,但重量非常大,也是不行的,因为运载能力是有限
的,所以第 1 种策略舍弃;如果选重量最小的物品装入,那么其价值不一定高,所以不能在总重限制的情况下保证价值最大,第 2 种策略舍弃;而第 3 种是每次选取单位重量价值最大的宝物,也就是说每次选择性价比(价值/重量)最高的宝物,如果可以达到运载重量 m,那么一定能得到价值最大。因此采用第 3 种贪心策略,每次从剩下的宝物中选择性价比最高的宝物。
算法设计
(1)数据结构及初始化。将 n 种宝物的重量和价值存储在结构体 three(包含重量、价
值、性价比 3 个成员)中,同时求出每种宝物的性价比也存储在对应的结构体 three 中,将其按照性价比从高到低排序。采用 sum 来存储毛驴能够运走的最大价值,初始化为 0。
(2)根据贪心策略,按照性价比从大到小选取宝物,直到达到毛驴的运载能力。每次选
择性价比高的物品,判断是否小于 m(毛驴运载能力),如果小于 m,则放入,sum(已放入物品的价值)加上当前宝物的价值,m 减去放入宝物的重量;如果不小于 m,则取该宝物的一部分 m * p[i],m=0,程序结束。m 减少到 0,则 sum 得到最大值。
完美图解
假设现在有一批宝物,价值和重量如表 2-3 所示,毛驴运载能力 m=30,那么怎么装入
最大价值的物品?
表 2-3 宝物清单
宝物 i 1 2 3 4 5 6 7 8 9 10
重量 w[i] 4 2 9 5 5 8 5 4 5 5
价值 v[i] 3 8 18 6 8 20 5 6 7 15
(1)因为贪心策略是每次选择性价比(价值/重量)高的宝物,可以按照性价比降序排
序,排序后如表 2-4 所示。
表 2-4 排序后宝物清单
宝物 i 2 10 6 3 5 8 9 4 7 1
重量 w[i] 2 5 8 9 5 4 5 5 5 4
价值 v[i] 8 15 20 18 8 6 7 6 5 3
性价比 p[i] 4 3 2.5 2 1.6 1.5 1.4 1.2 1 0.75
(2)按照贪心策略,每次选择性价比高的宝物放入:
第 1 次选择宝物 2,剩余容量 30−2=28,目前装入最大价值为 8。
第 2 次选择宝物 10,剩余容量 28−5=23,目前装入最大价值为 8+15=23。
第 3 次选择宝物 6,剩余容量 23−8=15,目前装入最大价值为 23+20=43。
第 4 次选择宝物 3,剩余容量 15−9=6,目前装入最大价值为 43+18=61。
第 5 次选择宝物 5,剩余容量 6−5=1,目前装入最大价值为 61+8=69。
第 6 次选择宝物 8,发现上次处理完时剩余容量为 1,而 8 号宝物重量为 4,无法全部
放入,那么可以采用部分装入的形式,装入 1 个重量单位,因为 8 号宝物的单位重量价值为1.5,因此放入价值 1×1.5=1.5,你也可以认为装入了 8 号宝物的 1/4,目前装入最大价值为69+1.5=70.5,剩余容量为 0。
(3)构造最优解
把这些放入的宝物序号组合在一起,就得到了最优解(2,10,6,3,5,8),其中最后
一个宝物为部分装入(装了 8 号财宝的 1/4),能够装入宝物的最大价值为 70.5。
伪代码详解
(1)数据结构定义
根据算法设计中的数据结构,我们首先定义一个结构体 three:
struct three{
double w; //每种宝物的重量
double v; //每种宝物的价值
double p; //每种宝物的性价比(价值/重量)
}
(2)性价比排序
我们可以利用 C++中的排序函数 sort(),对宝物的性价比从大到小(非递增)
排序。要使用此函数需引入头文件:
#include
语法描述为:
sort(begin, end)// 参数 begin 和 end 表示一个范围,分别为待排序数组的首地址和尾地址
在本例中我们采用结构体形式存储,按结构体中的一个字段,即按性价比排序。如果不
使用自定义比较函数,那么 sort 函数排序时不知道按哪一项的值排序,因此采用自定义比较
函数的办法实现宝物性价比的降序排序:
bool cmp(three a,three b)//比较函数按照宝物性价比降序排列
{
return a.p > b.p; //指明按照宝物性价比降序排列
}
sort(s, s+n, cmp); //前两个参数分别为待排序数组的首地址和尾地址
//最后一个参数 compare 表示比较的类型
(3)贪心算法求解
在性价比排序的基础上,进行贪心算法运算。如果剩余容量比当前宝物的重量大,则可以放入,剩余容量减去当前宝物的重量,已放入物品的价值加上当前宝物的价值。如果剩余
容量比当前宝物的重量小,表示不可以全部放入,可以切割下来一部分(正好是剩余容量),
然后令剩余容量乘以当前物品的单位重量价值,已放入物品的价值加上该价值,即为能放入
宝物的最大价值。
for(int i = 0;i < n;i++)//按照排好的顺序,执行贪心策略
{
if( m > s[i].w )//如果宝物的重量小于毛驴剩下的运载能力,即剩余容量
{
m -= s[i].w;
sum += s[i].v;
}
else //如果宝物的重量大于毛驴剩下的承载能力
{
sum += m 乘以 s[i].p; //进行宝物切割,切割一部分(m 重量),正好达到驴子承重
break;
}
}
#include<iostream>
#include<algorithm>
using namespace std;
const int M=1000005;
struct three{
double w;//每个宝物的重量
double v;//每个宝物的价值
double p;//性价比
}s[M];
bool cmp(three a,three b)
{
return a.p>b.p;//根据宝物的单位价值从大到小排序
}
int main()
{
int n;//n 表示有 n 个宝物
double m ;//m 表示毛驴的承载能力
cout<<"请输入宝物数量 n 及毛驴的承载能力 m :"<<endl;
cin>>n>>m;
cout<<"请输入每个宝物的重量和价值,用空格分开: "<<endl;
for(int i=0;i<n;i++)
{
cin>>s[i].w>>s[i].v;
s[i].p=s[i].v/s[i].w;//每个宝物单位价值
}
sort(s,s+n,cmp);
double sum=0.0;// sum 表示贪心记录运走宝物的价值之和
for(int i=0;i<n;i++)//按照排好的顺序贪心
{
if( m>s[i].w )//如果宝物的重量小于毛驴剩下的承载能力
{
m-=s[i].w;
sum+=s[i].v;
}
else//如果宝物的重量大于毛驴剩下的承载能力
{
sum+=m * s[i].p;//部分装入
break;
}
}
cout<<"装入宝物的最大价值 Maximum value="<<sum<<endl;
return 0;
}
4.数据库第三范式
第一、第二、第三范式之间的理解和比较
第一范式(1NF)无重复的列
所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。
1NF的定义为:符合1NF的关系中的每个属性都不可再分
下表所示情况,便不符合1NF的要求:
说明:在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。
第二范式(2NF)属性完全依赖于主键
第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。例如员工信息表中加上了员工编号(emp_id)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个惟一属性列被称为主关键字或主键、主码。
第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是属性完全依赖于主键。
第三范式(3NF)
满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在的员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。 也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3NF的要求。
总结
我对以上问题做了总结和复习,感觉又是那么熟悉,以后也要专注基础知识的学习,最后希望能进入湖南大学学习。