我的技术博客:www.zhenganwen.topjava
时间复杂度是衡量算法好坏的重要指标之一。时间复杂度反映的是不肯定性样本量的增加对于算法操做所需时间的影响程度,与算法操做是否涉及到样本量以及涉及了几回直接相关,如遍历数组时时间复杂度为数组长度n(对应时间复杂度为O(n)
),而对数据的元操做(如加减乘除与或非等)、逻辑操做(如if判断)等都属于常数时间内的操做(对应时间复杂度O(1)
)。node
在化简某算法时间复杂度表达式时需遵循如下规则:面试
O(n^2)+O(n)
可化简为O(n^2)
,O(n)+O(1)
可化简为O(n)
O(2n)
可化简为O(n)
,O(8)
可化简为O(1)
O(logm)+O(n^2)
不能化简为O(n^2)
或O(logm)
。而要视m、n二者之间的差距来化简,好比m>>n时能够化简为O(logm)
,由于表达式增量是由样本量决定的。算法额外空间复杂度指的是对于输入样本,通过算法操做须要的额外空间。好比使用冒泡排序对一个数组排序,期间只须要一个临时变量temp
,那么该算法的额外空间复杂度为O(1)
。又如归并排序,在排序过程当中须要建立一个与样本数组相同大小的辅助数组,尽管在排序事后该数组被销毁,但该算法的额外空间复杂度为O(n)
。算法
找出数组B中不属于A的数,数组A有序而数组B无序。假设数组A有n个数,数组B有m个数,写出算法并分析时间复杂度。api
首先遍历B,将B中的每一个数拿到到A中找,若找到则打印。对应算法以下:数组
int A[] = {1, 2, 3, 4, 5};
int B[] = {1, 4, 2, 6, 5, 7};
for (int i = 0; i < 6; ++i) {
int temp = B[i];
bool flag = false;
for (int j = 0; j < 5; ++j) {
if (A[j] == temp) {
flag = true; //找到了
break;
}
}
if (!flag) { //没找到
printf("%d", temp);
}
}
复制代码
不难看出上述算法的时间复杂度为O(m*n)
,由于将两个数组都遍历了一遍缓存
因为数组A是有序的,在一个有序序列中查找一个元素可使用二分法(也称折半法)。原理就是将查找的元素与序列的中位数进行比较,若是小于则去掉中位数及其以后的序列,若是大于则去掉中位数及其以前的序列,若是等于则找到了。若是不等于那么再将其与剩下的序列继续比较直到找到或剩下的序列为空为止。安全
利用二分法对应题解的代码以下:bash
for (int i = 0; i < 6; ++i) { //B的长度为6
int temp = B[i];
//二分法查找
int left = 0,right = 5-1; //A的长度为5
int mid = (left + right) / 2;
while (left < right && A[mid] != temp) {
if (A[mid] > temp) {
right = mid - 1;
} else {
left = mid + 1;
}
mid = (left + right) / 2;
}
if (A[mid] != temp) {
printf("%d", temp);
}
}
复制代码
for
循环m
次,while
循环logn
次(若是没有特别说明,log均以2为底),此算法的时间复杂度为O(mlogn)
数据结构
第三种方法就是将数组B也排序,而后使用逐次比对的方式来查找A数组中是否含有B数组中的某元素。引入a、b两个指针分别指向数组A、B的首元素,比较指针指向的元素值,当a<b
时,向后移动a指针查找该元素;当a=b
时,说明A中存在该元素,跳过该元素查找,向后移动b;当a>b
时说明A中不存在该元素,打印该元素并跳过该元素的查找,向后移动b。直到a或b有一个到达数组末尾为止(若a先到达末尾,那么b和b以后的数都不属于A)
对应题解的代码以下:
void fun3(int A[],int a_length,int B[],int b_length){
quickSort(B, 0, b_length - 1); //使用快速排序法对数组B排序->O(mlogm)
int* a = A,*b=B;
while (a <= A + a_length - 1 || b <= B + b_length - 1) {
if (*a == *b) {
b++;
continue;
}
if (*a > *b) {
printf("%d", *b);
b++;
} else {
a++;
}
}
if (a == A + a_length) { //a先到头
while (b < B + b_length) {
printf("%d", *b);
b++;
}
}
}
复制代码
快速排序的代码以下:
#include <stdlib.h>
#include <time.h>
//交换两个int变量的值
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
//产生一个low~high之间的随机数
int randomInRange(int low, int high){
srand((int) time(0));
return (rand() % (high - low))+low;
}
//快速排序的核心算法,随机选择一个数,将比该数小的移至数组左边,比该数大的移至
//数组右边,最后返回该数的下标(移动完以后该数的下标可能与移动以前不同)
int partition(int arr[],int start,int end){
if (arr == NULL || start < 0 || end <= 0 || start > end) {
return -1;
}
int index = randomInRange(start, end);//随机选择一个数
swap(arr[index], arr[end]);//将该数暂时放至末尾
int small = start - 1;
//遍历前n-1个数与该数比较并以该数为界限将前n-1个数
//分为两组,small指向小于该数的那一组的最后一个元素
for (index = start; index < end; index++) {
if (arr[index] < arr[end]) {
small++;
if (small != index) {
swap(arr[small], arr[index]);
}
}
}
//最后将该数放至数值较小的那一个组的中间
++small;
swap(arr[small], arr[end]);
return small;
}
void quickSort(int arr[],int start,int end) {
if (start == end) {
return;
}
int index = partition(arr, start, end);
if (index > start) {
quickSort(arr,start, index - 1);
}
if (index < end) {
quickSort(arr, index + 1, end);
}
}
复制代码
此种方法的时间复杂度为:O(mlogm)
(先对B排序)+O(m+n)
(最坏的状况是指针a和b都到头)。
O(m*n)
O(mlogn)
(以2为底)O(mlogm)+O(m+n)
(以2为底)易知算法2比1更优,由于增加率logn<n
。而2和3的比较取决于样本量m和n之间的差距,若m>>n
那么2更优,不难理解:数组B元素较多,那么对B的排序确定要花费较长时间,而这一步并非题解所必需的,不如采用二分法;相反地,若m<<n
,那么3更优。
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
思路:利用两个指针L
、R
,将L
指向首元素以前,将R
指向尾元素以后。从头遍历序列,将当前遍历元素与num
比较,若num
,则将其与L
的右一个元素交换位置并遍历下一个元素、右移L
;若=num
则直接遍历下一个元素;若>num
则将其和R
的左一个元素交换位置,并从新判断当前位置元素与num
的关系。直到遍历的元素下标到为R-1
为止。
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
void partition(int arr[],int startIndex,int endIndex,int num){
int L = startIndex - 1, R = endIndex + 1, i = startIndex;
while (i <= R - 1) {
if (arr[i] < num) {
swap(arr[i++], arr[++L]);
} else if (arr[i] > num) {
swap(arr[i], arr[--R]);
} else {
i++;
}
}
}
int main(){
int arr[] = {1,2, 1, 5, 4, 7, 2, 3, 9,1};
travles(arr, 8);
partition(arr, 0, 7, 2);
travles(arr, 8);
return 0;
}
复制代码
L
表明小于num
的数的右界,R
表明大于num
的左界,partition
的过程就是遍历元素、不断壮大L、R
范围的过程。这里比较难理解的地方多是为何arr[i]<num
时要右移L
而arr[i]>num
时却不左移R
,这是由于对于当前元素arr[i]
,若是arr[i]<num
进行swap(arr[i],arr[L+1])
以后对于当前下标的数据情况是知晓的(必定有arr[i]=arr[L+1]
),由于是从头遍历到i
的,而L+1<=i
。可是若是arr[i]>num
进行swap(arr[i],arr[R-1])
以后对于当前元素的数据情况是不清楚的,由于R-1>=i
,arr[R-1]
还没遍历到。
给定一个4阶矩阵以下:
打印结果以下(要求额外空间复杂度为O(1)
):
1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10
复制代码
思路:这类问题须要将思惟打开,从宏观的层面去找出问题存在的共性从而求解。若是你的思惟局限在1是如何变到2的、4是怎么变到8的、11以后为何时十、它们之间有什么关联,那么你就陷入死胡同了。
从宏观的层面找共性,其实转圈打印的过程就是不断顺时针打印外围元素的过程,只要给你一个左上角的点(如
(0,0)
)和右下角的点(如(3,3)
),你就可以打印出1 2 3 4 8 12 16 15 14 13 9 5
;一样,给你(1,1)
和(2,2)
,你就能打印出6 7 11 10
。这个根据两点打印正方形上元素的过程能够抽取出来,整个问题也就迎刃而解了。
打印一个矩阵某个正方形上的点的逻辑以下:
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#define FACTORIAL 4
void printSquare(int leftUp[], int rigthDown[],int matrix[][FACTORIAL]){
int i = leftUp[0], j = leftUp[1];
while (j < rigthDown[1]) {
printf("%d ", matrix[i][j++]);
}
while (i < rigthDown[0]) {
printf("%d ", matrix[i++][j]);
}
while (j > leftUp[1]) {
printf("%d ", matrix[i][j--]);
}
while (i > leftUp[0]) {
printf("%d ", matrix[i--][j]);
}
}
void printMatrixCircled(int matrix[][FACTORIAL]){
int leftUp[] = {0, 0}, rightDown[] = {FACTORIAL-1,FACTORIAL-1};
while (leftUp[0] < rightDown[0] && leftUp[1] < rightDown[1]) {
printSquare(leftUp, rightDown, matrix);
++leftUp[0];
++leftUp[1];
--rightDown[0];
--rightDown[1];
}
}
int main(){
int matrix[4][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
printMatrixCircled(matrix);//1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10
}
复制代码
给定一个方块矩阵,请把该矩阵调整成顺时针旋转90°以后的样子,要求额外空间复杂度为O(1)
。
思路:拿上图举例,首先选取矩阵四个角上的点
1,3,9,7
,按顺时针的方向1
到3
的位置(1->3
)、3->9
、9->7
、7->1
,这样对于旋转后的矩阵而言,这四个点已经调整好了。接下来只需调整2,6,8,4
的位置,调整方法是同样的。只需对矩阵第一行的前n-1个点采用一样的方法进行调整、对矩阵第二行的前前n-3个点……,那么调整n阶矩阵就容易了。这也是在宏观上观察数据变更的通常规律,找到以不变应万变的通解(给定一个点,肯定矩阵上以该点为角的正方形,将该正方形旋转90°),整个问题就不攻自破了。
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#define FACTORIAL 4
void circleSquare(int leftUp[],int rightDown[],int matrix[][FACTORIAL]){
int p1[] = {leftUp[0], leftUp[1]};
int p2[] = {leftUp[0], rightDown[1]};
int p3[] = {rightDown[0], rightDown[1]};
int p4[] = {rightDown[0],leftUp[1]};
while (p1[1] < rightDown[1]) {
//swap
int tmp = matrix[p4[0]][p4[1]];
matrix[p4[0]][p4[1]] = matrix[p3[0]][p3[1]];
matrix[p3[0]][p3[1]] = matrix[p2[0]][p2[1]];
matrix[p2[0]][p2[1]] = matrix[p1[0]][p1[1]];
matrix[p1[0]][p1[1]] = tmp;
p1[1]++;
p2[0]++;
p3[1]--;
p4[0]--;
}
}
void circleMatrix(int matrix[][FACTORIAL]){
int leftUp[] = {0, 0}, rightDown[] = {FACTORIAL - 1, FACTORIAL - 1};
while (leftUp[0] < rightDown[0] && leftUp[1] < rightDown[1]) {
circleSquare(leftUp, rightDown, matrix);
leftUp[0]++;
leftUp[1]++;
--rightDown[0];
--rightDown[1];
}
}
void printMatrix(int matrix[][FACTORIAL]){
for (int i = 0; i < FACTORIAL; ++i) {
for (int j = 0; j < FACTORIAL; ++j) {
printf("%2d ", matrix[i][j]);
}
printf("\n");
}
}
int main(){
int matrix[FACTORIAL][FACTORIAL] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
printMatrix(matrix);
circleMatrix(matrix);
printMatrix(matrix);
}
复制代码
对如上矩阵的打印结果以下(要求额外空间复杂度为O(1)
):
1 2 7 13 8 3 4 9 14 15 10 5 6 11 16 17 12 18
复制代码
此题也是须要从宏观上找出一个共性:给你两个,你可否将该两点连成的45°斜线上的点按给定的打印方向打印出来。拿上图举例,给出
(2,0)
、(0,2)
和turnUp=true
,应该打印出13,8,3
。那么整个问题就变成了两点的走向问题了,开始时两点均为(0,0)
,而后一个点往下走,另外一个点往右走(如1->7
,1->2
);当往下走的点是边界点时就往右走(如13->14
),当往右走的点到边界时就往下走(如6->12
)。每次两点走一步,并打印两点连线上的点。
//
// Created by zaw on 2018/10/22.
//
#include <stdio.h>
const int rows = 3;
const int cols = 6;
void printLine(int leftDown[],int rightUp[], bool turnUp,int matrix[rows][cols]){
int i,j;
if (turnUp) {
i = leftDown[0], j = leftDown[1];
while (j <= rightUp[1]) {
printf("%d ", matrix[i--][j++]);
}
} else {
i = rightUp[0], j = rightUp[1];
while (i <= leftDown[0]) {
printf("%d ", matrix[i++][j--]);
}
}
}
void zigZagPrintMatrix(int matrix[rows][cols]){
if (matrix==NULL)
return;
int leftDown[] = {0, 0}, rightUp[] = {0, 0};
bool turnUp = true;
while (leftDown[1] <= cols - 1) {
printLine(leftDown, rightUp, turnUp, matrix);
turnUp = !turnUp;
if (leftDown[0] < rows - 1) {
leftDown[0]++;
} else {
leftDown[1]++;
}
if (rightUp[1] < cols - 1) {
++rightUp[1];
} else {
++rightUp[0];
}
}
}
int main(){
int matrix[rows][cols] = {
{1, 2, 3, 4, 5, 6},
{7, 8, 9, 10, 11, 12},
{13, 14, 15, 16, 17, 18}
};
zigZagPrintMatrix(matrix);//1 2 7 13 8 3 4 9 14 15 10 5 6 11 16 17 12 18
return 0;
}
复制代码
如图:
任何一列或一行上的数是有序的,实现一个函数,判断某个数是否存在于矩阵中。要求时间复杂度为O(M+N)
,额外空间复杂度为O(1)
。
从矩阵右上角的点开始取点与该数比较,若是大于该数,那么说明这个点所在的列都不存在该数,将这个点左移;若是这个点上的数小于该数,那么说明这个点所在的行不存在该数,将这个点下移。直到找到与该数相等的点为止。最坏的状况是,该数只有一个且在矩阵左下角上,那么时间复杂度为
O(M-1+N-1)=O(M+N)
//
// Created by zaw on 2018/10/22.
//
#include <stdio.h>
const int rows = 4;
const int cols = 4;
bool findNumInSortedMatrix(int num,int matrix[rows][cols]){
int i = 0, j = cols - 1;
while (i <= rows - 1 && j <= cols - 1) {
if (matrix[i][j] > num) {
--j;
} else if (matrix[i][j] < num) {
++i;
} else {
return true;
}
}
return false;
}
int main(){
int matrix[rows][cols] = {
{1, 2, 3, 4},
{2, 4, 5, 8},
{3, 6, 7, 9},
{4, 8, 9, 10}
};
if (findNumInSortedMatrix(7, matrix)) {
printf("find!");
} else {
printf("not exist!");
}
return 0;
}
复制代码
一个矩阵中只有0和1两种值,每一个位置均可以和本身的上、下、左、右四个位置相连,若是有一片1连在一块儿,这个部分叫作一个岛,求一个矩阵中有多少个岛?
好比矩阵:
1 | 0 | 1 |
---|---|---|
0 | 1 | 0 |
1 | 1 | 1 |
就有3个岛。
分析:咱们能够遍历矩阵中的每一个位置,若是遇到1就将与其相连的一片1都感染成2,并自增岛数量。
public class IslandNum {
public static int getIslandNums(int matrix[][]){
int res = 0 ;
for(int i = 0 ; i < matrix.length ; i++){
for(int j = 0 ; j < matrix[i].length ; j++){
if(matrix[i][j] == 1){
res++;
infect(matrix , i , j);
}
}
}
return res;
}
public static void infect(int matrix[][], int i ,int j){
if(i < 0 || i >= matrix.length || j < 0 || j >= matrix[i].length || matrix[i][j] != 1){
return;
}
matrix[i][j] = 2;
infect(matrix , i-1 , j);
infect(matrix , i+1 , j);
infect(matrix , i , j-1);
infect(matrix , i , j+1);
}
public static void main(String[] args){
int matrix[][] = {
{1,0,0,1,0,1},
{0,1,1,0,0,0},
{1,0,0,0,1,1},
{1,1,1,1,1,1}
};
System.out.println(getIslandNums(matrix));
}
}
复制代码
KMP算法是由一个问题而引起的:对于一个字符串str
(长度为N)和另外一个字符串match
(长度为M),若是match
是str
的子串,请返回其在str
第一次出现时的首字母下标,若match
不是str
的子串则返回-1
。
最简单的方法是将str
从头开始遍历并与match
逐次比较,若碰到了不匹配字母则终止这次遍历转而从str
的第二个字符开始遍历并与match
逐次比较,直到某一次的遍历每一个字符都与match
匹配不然返回-1
。易知此种作法的时间复杂度为O(N*M)
。
KMP算法则给出求解该问题时间复杂度控制在
O(N)
的解法。
首先该算法须要对应match
建立一个与match
长度相同的辅助数组help[match.length]
,该数组元素表示match
某个下标以前的子串的先后缀子串最大匹配长度。前缀子串表示一个串中以串首字符开头的不包含串尾字符的任意个连续字符,后缀子串则表示一个串中以串尾字符结尾的不包括串首字符的任意个连续字符。好比abcd
的前缀子串能够是a
、ab
、abc
,但不能是abcd
,而abcd
的后缀字串能够是d
、cd
、bcd
,但不能是abcd
。再来讲一下help
数组,对于char match[]="abc1abc2"
来讲,有help[7]=3
,由于match[7]='2'
,所以match
下标在7
以前的子串abc1abc
的前缀子串和后缀子串相同的状况下,前缀子串的最大长度为3(即前缀字串和后缀字串都取abc
);又如match="aaaab"
,有help[4]=3
(前缀子串和后缀子串最大匹配长度当二者为aaa
时取得),相应的有help[3]=2
、help[2]=1
。
假设当要寻找的子串match
的help
数组找到以后(对于一个串的help
数组的求法在介绍完KMP
算法以后再详细说明)。就能够进行KMP
算法求解此问题了。KMP
算法的逻辑(结论)是,对于str
的i~(i+k)
部分(i
、i+k
均为str
的合法下标)和match
的0~k
部分(k
为match
的合法下标),若是有str[i]=match[0]
、str[i+1]=match[1]
……str[i+k-1]=match[k-1]
,但str[i+k]!=[k]
,那么str
的下标不用从i+k
变为i+1
从新比较,只需将子串str[0]~str[i+k-1]
的最大匹配前缀子串的后一个字符cn
从新与str[i+k]
向后依次比较,后面若是又遇到了不匹配的字符重复此操做便可:
当遇到不匹配字符时,常规的作法是将str
的遍历下标sIndex
移到i+1
的位置并将match
的遍历下标mIndex
移到0
再依次比较,这种作法并无利用上一轮的比较信息(对下一轮的比较没有任何优化)。而KMP
算法则不是这样,当遇到不匹配的字符str[i+k]
和match[k]
时,str
的遍历指针sIndex=i+k
不用动,将match
右滑并将其遍历指针mIndex
打到子串match[0]~match[k-1]
的最大匹配前缀子串的后一个下标n
的位置。而后sIndex
从i+k
开始,mIndex
从n
开始,依次向后比较,若再遇到不匹配的数则重复此过程。
对应代码以下:
void length(char* str){
if(str==NULL)
return -1;
int len=0;
while(*(str++)!='\0'){
len++;
}
return len;
}
int getIndexOf(char* str,char* m){
int slen = length(str) , mlen = length(m);
if(mlen > slen)
return -1;
int help[mlen];
getHelpArr(str,help);
int i=0,j=0; //sIndex,mIndex
while(i < slen && j < mlen){
if(str[i] == m[j]){
i++;
j++;
}else if(help[j] != -1){
j = help[j]; //mIndex -> cn's index
}else{ //the first char is not match,move the sIndex
i++;
}
}
return j == mlen ? i - mlen : -1;
}
复制代码
能够发现KMP
算法中str
的遍历指针并无回溯这个动做(只向后移动),当完成匹配时sIndex
的移动次数小于N
,不然sIndex
移动到串尾也会终止循环,因此while
对应的匹配过程的时间复杂度为O(N)
(if(help[j] != -1){ j = help[j] }
的执行次数只会是常数次,所以能够忽略)。
下面只要解决如何求解一个串的help
数组,此问题就解决了。help
数组要从前到后求解,直接求help[n]
是很难有所头绪的。当串match
长度mlen=1
时,规定help[0]=-1
。当mlen=2
时,去掉match[1]
以后只剩下match[0]
,最大匹配子串长度为0(由于前缀子串不能包含串尾字符,后缀子串不能包含串首字符),即help[1]=0
。当mlen>2
时,help[n]
(n>=2)均可以推算出来:
如上图所示,若是咱们知道了help[n-1]
,那么help[n]
的求解有两种状况:若是match[cn]=match[n-1]
,那么由a区域与b区域(a、b为子串match[0~n-2]
的最大匹配前缀子串和后缀字串)相同可知help[n]=help[n-1]+1
;若是match[cn]!=match[n-1]
,那么求a区域中下一个能和b区域后缀子串中匹配的较大的一个,即a区域的最大匹配前缀字串c区域
,将match[n-1]
和c区域的后一个位置(cn'
)上的字符比较,若是相等则help[n]
等于c区域的长度+1,而c区域的长度就是help[cn]
(help
数组的定义如此);若是不等则将cn
打到cn'
的位置继续和match[n-1]
比较,直到cn
被打到0
为止(即help[cn]=-1
为止),那么此时help[n]=0
。
对应代码以下:
int* getHelpArr(char* s,int help[]){
if(s==NULL)
return NULL;
int slen = length(s);
help[0]=-1;
help[1]=0;
int index = 2;//help数组从第三个元素开始的元素值须要依次推算
int cn = 0; //推算help[2]时,help[1]=0,即s[1]以前的字符组成的串中不存在最大匹配先后子串,那么cn做为最大匹配前缀子串的后一个下标天然就是0了
while(index < slen){
if(s[index-1] == s[cn]){ //if match[n-1] == match[cn]
help[index] = help[index-1] + 1;
index++;
cn++;
}else if(help[cn] == -1){ //cn reach 0
help[index]=0;
index++;
cn++;
}else{
cn = help[cn]; //set cn to cn' and continue calculate help[index]
}
}
return help;
}
复制代码
那么这个求解help
数组的过程的时间复杂度如何计算呢?仔细观察克制while
循环中仅涉及到index
和cn
这两个变量的变化:
第一个if分支 | 第二个if分支 | 第三个if分支 | |
---|---|---|---|
index | 增大 | 增大 | 不变 |
index-cn | 不变 | 不变 | 增大 |
能够发现while
循环执行一次不是index
增大就是index-cn
增大,而index < slen
、index - cn < slen
,即index
最多自增M
(match
串的长度)次 ,index-cn
最多增长M
次,如此while
最多执行M+M
次,即时间复杂为O(2M)=O(M)
。
综上所述,使用KMP
求解此问题的时间复杂度为O(M)
(求解match
的help
数组的时间复杂度)+O(N)
(匹配的时间复杂度)=O(N)
(由于N > M
)。
判断一个二叉树是不是另外一棵二叉树的子树(即某棵树的结构和数据状态和另外一棵二叉树的子树样)。
思路:若是这棵树的序列化串是另外一棵树的序列化串的子串,那么前者一定是后者的子树。
前缀树是一种存储字符串的高效容器,基于此结构的操做有:
insert
插入一个字符串到容器中
search
容器中是否存在某字符串,返回该字符串进入到容器的次数,没有则返回0
delete
将某个字符串进入到容器的次数减1
prefixNumber
返回全部插入操做中,以某个串为前缀的字符串出现的次数
设计思路:该结构的重点实如今于存储。前缀树以字符为存储单位,将其存储在结点之间的树枝上而非结点上,如插入字符串abc
以后前缀树以下:
每次插入串都要从头结点开始,遍历串中的字符依次向下“铺路”,如上图中的abc
3条路。对于每一个结点而言,它能够向下铺a~z
26条不一样的路,假如来到某个结点后,它要向下铺的路(取决于遍历到哪一个字符来了)被以前插入串的过程铺过了那么就能够直接走这条路去往下一个结点,不然就要先铺路再去往下一个结点。如再插入串abde
和bcd
的前缀树将以下所示:
根据前缀树的search
和prefixNumber
两个操做,咱们还须要在每次铺路后记录如下每一个结点通过的次数(across
),以及每次插入操做每一个结点做为终点结点的次数(end
)。
前缀树的实现示例:
public class TrieTree {
public static class TrieNode {
public int across;
public int end;
public TrieNode[] paths;
public TrieNode() {
super();
across = 0;
end = 0;
paths = new TrieNode[26];
}
}
private TrieNode root;
public TrieTree() {
super();
root = new TrieNode();
}
//向树中插入一个字符串
public void insert(String str) {
if (str == null || str.length() == 0) {
return;
}
char chs[] = str.toCharArray();
TrieNode cur = root;
for (char ch : chs) {
int index = ch - 'a';
if (cur.paths[index] == null) {
cur.paths[index] = new TrieNode();
}
cur = cur.paths[index];
cur.across++;
}
cur.end++;
}
//查询某个字符串插入的次数
public int search(String str) {
if (str == null || str.length() == 0) {
return 0;
}
char chs[] = str.toCharArray();
TrieNode cur = root;
for (char ch : chs) {
int index = ch - 'a';
if (cur.paths[index] == null) {
return 0;
}else{
cur = cur.paths[index];
}
}
return cur.end;
}
//删除一次插入过的某个字符串
public void delete(String str) {
if (search(str) > 0) {
char chs[] = str.toCharArray();
TrieNode cur = root;
for (char ch : chs) {
int index = ch - 'a';
if (--cur.paths[index].across == 0) {
cur.paths[index] = null;
return;
}
cur = cur.paths[index];
}
cur.end--;
}
}
//查询全部插入的字符串中,以prefix为前缀的有多少个
public int prefixNumber(String prefix) {
if (prefix == null || prefix.length() == 0) {
return 0;
}
char chs[] = prefix.toCharArray();
TrieNode cur = root;
for (char ch : chs) {
int index = ch - 'a';
if (cur.paths[index] == null) {
return 0;
}else{
cur = cur.paths[index];
}
}
return cur.across;
}
public static void main(String[] args) {
TrieTree tree = new TrieTree();
tree.insert("abc");
tree.insert("abde");
tree.insert("bcd");
System.out.println(tree.search("abc")); //1
System.out.println(tree.prefixNumber("ab")); //2
}
}
复制代码
一个字符串类型的数组arr1,另外一个字符串类型的数组arr2:
冒泡排序的核心是从头遍历序列。以升序排列为例:将第一个元素和第二个元素比较,若前者大于后者,则交换二者的位置,再将第二个元素与第三个元素比较,若前者大于后者则交换二者位置,以此类推直到倒数第二个元素与最后一个元素比较,若前者大于后者,则交换二者位置。这样一轮比较下来将会把序列中最大的元素移至序列末尾,这样就安排好了最大数的位置,接下来只需对剩下的(n-1)个元素,重复上述操做便可。
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
void bubbleSort(int arr[], int length) {
if(arr==NULL || length<=1){
return;
}
for (int i = length-1; i > 0; i--) { //只需比较(length-1)轮
for (int j = 0; j < i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
复制代码
该算法的时间复杂度为n+(n-1)+...+1
,很明显是一个等差数列,由(首项+末项)*项数/2求其和为(n+1)n/2
,可知时间复杂度为O(n^2)
以升序排序为例:找到最小数的下标minIndex
,将其与第一个数交换,接着对子序列(1-n)重复该操做,直到子序列只含一个元素为止。(即选出最小的数放到第一个位置,该数安排好了,再对剩下的数选出最小的放到第二个位置,以此类推)
void selectionSort(int arr[], int length) {
for (int i = 0; i < length-1; ++i) { //要进行n-1次选择,选出n-1个数分别放在前n-1个位置上
if(arr==NULL || length<=1){
return;
}
int minIndex = i; //记录较小数的下标
for (int j = i+1; j < length; ++j) {
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
if (minIndex != i) {
swap(&arr[minIndex],&arr[i]);
}
}
}
复制代码
一样,不可贵出该算法的时间复杂度(big o)为O(n^2)
(n-1+n-2+n-3+…+1)
插入排序的过程能够联想到打扑克时揭一张牌而后将其到手中有序纸牌的合适位置上。好比我如今手上的牌是七、八、九、J、Q、K,这时揭了一张10,我须要将其依次与K、Q、J、九、八、7比较,当比到9时发现大于9,因而将其插入到9以后。对于一个无序序列,能够将其当作一摞待揭的牌,首先将首元素揭起来,由于揭以前手上无牌,所以这次揭牌无需比较,此后每揭一次牌都须要进行上述的插牌过程,当揭完以后,手上的握牌顺序就对应着该序列的有序形式。
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
void insertionSort(int arr[], int length){
if(arr==NULL || length<=1){
return;
}
for (int i = 1; i < length; ++i) { //第一张牌无需插入,直接入手,后续揭牌需比较而后插入,所以从第二个元素开始遍历(插牌)
//将新揭的牌与手上的逐次比较,若小于则交换,不然中止,比较完了还没遇到更小的也中止
for (int j = i - 1; j >= 0 || arr[j] <= arr[j + 1]; j--) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
复制代码
插入排序的big o该如何计算?能够发现若是序列有序,那么该算法的big o为O(n)
,由于只是遍历了一次序列(这时最好状况);若是序列降序排列,那么该算法的big o为O(n^2)
(每次插入前的比较交换加起来要:1+2+…+n-1)(最坏状况)。**通常应用场景中都是按算法的最坏状况来考量算法的效率的,由于你作出来的应用要可以承受最坏状况。**即该算法的big o为O(n^2)
归并排序的核心思想是先让序列的左半部分有序、再让序列的右半部分有序,最后从两个子序列(左右两半)从头开始逐次比较,往辅助序列中填较小的数。
以序列{2,1,4,3}
为例,归并排序的过程大体以下:
算法代码示例:
void merge(int arr[],int helpArr[], int startIndex, int midIndex,int endIndex) {
int L = startIndex, R = midIndex + 1, i = startIndex;
while (L <= midIndex && R <= endIndex) { //只要没有指针没越界就逐次比较
helpArr[i++] = arr[L] < arr[R] ? arr[L++] : arr[R++];
}
while (L != midIndex + 1) {
helpArr[i++] = arr[L++];
}
while (R != endIndex + 1) {
helpArr[i++] = arr[R++];
}
for (i = startIndex; i <= endIndex; i++) {
arr[i] = helpArr[i];
}
}
void mergeSort(int arr[],int helpArr[], int startIndex, int endIndex) {
int midIndex;
if (startIndex < endIndex) { //当子序列只含一个元素时,再也不进行此子过程
//(endIndex+startIndex)/2可能会致使int溢出,下面求中位数的作法更安全
midIndex = startIndex + ((endIndex - startIndex) >> 1);
mergeSort(arr, helpArr, startIndex, midIndex); //对左半部分排序
mergeSort(arr, helpArr, midIndex + 1, endIndex); //对右半部分排序
merge(arr, helpArr, startIndex, midIndex, endIndex); //使总体有序
}
}
int main(){
int arr[] = {9, 1, 3, 4, 7, 6, 5};
travels(arr, 7);//遍历打印
int helpArr[7];
mergeSort(arr, helpArr, 0, 7);
travels(arr, 7);
return 0;
}
复制代码
此算法的核心就是第2四、2五、26
这三行。第26
行应该不难理解,就是使用两个指针L、R
外加一个辅助数组,将两个序列有序地并入辅助数组。但为何2四、25
行执行事后数组左右两半部分就分别有序了呢?这就又牵扯到了归并排序的核心思想:先让一个序列左右两半部分有序,而后再并入使总体有序。所以2四、25
是对左右两半部分分别递归执行归并排序,直到某次递归时左右两半部分均为一个元素时递归终止。当一个序列只含两个元素时,调用mergeSort
会发现2四、25
行是无效操做,直接执行merge
。就像上图所示,两行递归完毕后,左右两半部分都会变得有序。
当一个递归过程比较复杂时(不像递归求阶乘那样一幕了然),咱们能够列举简短样本进行分析。
对于这样复杂的递归行为,千万不要想着追溯整个递归过程,只需分析第一步要作的事(好比此例中第一步要作的是就是
mergeSort
函数所呈现出来的那样:对左半部分排序、对右半部分排序、最后并入,你先无论是怎么排序的,不要被2四、25行的mergeSort
给带进去了)和递归终止的条件(好比此例中是``startIndex>=endIndex`,即要排序的序列只有一个元素时)。
归并排序的时间复杂度是O(nlogn)
,额外空间复杂度是O(n)
。
根据Master公式(本文 小技巧一节中有讲到)可得T(n)=2T(n/2)+O(n)
,第一个2的含义是子过程(对子序列进行归并排序)要执行两次,第二个2的含义是子过程样本量占一半(由于分红了左右两半部分),最后O(n)
表示左右有序以后进行的并入操做为O(n+n)=O(n)
(L、R指针移动次数总和为n,将辅助数组覆盖源数组为n),符合T(n)=aT(n/b)+O(n^d)
,经计算该算法的时间复杂度为O(nlogn)
在一个数组中,每个数左边比当前数小的数累加起来,叫作这个数组的小和。求一个数组的小和。例如:
对于数组[1,3,4,2,5]
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,一、3;
2左边比2小的数,1;
5左边比5小的数,一、三、四、2;
因此小和为1+1+3+1+1+3+4+2=16
复制代码
简单的作法就是遍历一遍数组,将当前遍历的数与该数以前数比较并记录小于该数的数。易知其时间复杂度为O(n^2)
(0+1+2+……+n-1)。
更优化的作法是利用归并排序的并入逻辑:
对应代码:
int merge(int arr[],int helpArr[], int startIndex, int midIndex,int endIndex) {
int L = startIndex, R = midIndex + 1, i = startIndex;
int res=0;
while (L <= midIndex && R <= endIndex ) { //只要没有指针没越界就逐次比较
res += arr[L] < arr[R] ? arr[L] * (endIndex - R + 1) : 0;
helpArr[i++] = arr[L] < arr[R] ? arr[L++] : arr[R++];
}
while (L != midIndex + 1) {
helpArr[i++] = arr[L++];
}
while (R != endIndex + 1) {
helpArr[i++] = arr[R++];
}
for (i = startIndex; i <= endIndex; i++) {
arr[i] = helpArr[i];
}
return res;
}
int mergeSort(int arr[],int helpArr[], int startIndex, int endIndex) {
int midIndex;
if (startIndex < endIndex) { //当子序列只含一个元素时,再也不进行此子过程
midIndex = startIndex + ((endIndex - startIndex) >> 1);
return mergeSort(arr, helpArr, startIndex, midIndex) + //对左半部分排序
mergeSort(arr, helpArr, midIndex + 1, endIndex) + //对右半部分排序
merge(arr, helpArr, startIndex, midIndex, endIndex); //使总体有序
}
return 0; //一个元素时不存在小和
}
int main(){
int arr[] = {1,3,4,2,5};
int helpArr[5];
printf("small_sum:%d\n",mergeSort(arr, helpArr, 0, 4)) ;
return 0;
}
复制代码
该算法在归并排序的基础上作了略微改动,即merge
中添加了变量res
记录每次并入操做应该累加的小和、mergeSort
则将每次并入应该累加的小和汇总。此种作法的复杂度与归并排序的相同,优于遍历的作法。能够理解,依次求每一个数的小和过程当中有不少比较是重复的,而利用归并排序求小和时利用了并入的两个序列分别有序的特性省去了没必要要的比较,如134并入25
时,2>1
直接推出2
后面的数都>1
,所以直接1*(endIndex-indexOf(2)+1)
便可。这在样本量不大的状况下看不出来优化的效果,试想一下若是样本量为2^32
,那么依照前者求小和O(n^2)
可知时间复杂度为O(21亿的平方)
,而归并排序求小和则只需O(21亿*32)
,足以见得O(n^2)
和O(nlogn)
的优劣。
在一个数组中,左边的数若是比右边的数大,则这两个数构成一个逆序对,请打印全部逆序对。
这题的思路也能够利用归并排序来解决,在并入操做时记录
arr[L]>arr[R]
的状况便可。
经典快排就是将序列中比尾元素小的移动到序列左边,比尾元素大的移动到序列右边,对以该元素为界的左右两个子序列(均不包括该元素)重复此操做。
首先咱们要考虑的是对给定的一个数,如何将序列中比该数小的移动到左边,比该数大的移动到右边。
思路:利用一个辅助指针
small
,表明较小数的右边界(初始指向首元素前一个位置),遍历序列每次遇到比该数小的数就将其与arr[small+1]
交换并右移small
,最后将该数与arr[small+1]
交换即达到目的。对应算法以下:
void partition(int arr[], int startIndex, int endIndex){
int small = startIndex - 1;
for (int i = startIndex; i < endIndex; ++i) {
if(arr[i] < arr[endIndex]) {
if (small + 1 != i) {
swap(arr[++small], arr[i]);
} else {
//若是small、i相邻则不用交换
small++;
}
}
}
swap(arr[++small], arr[endIndex]);
}
int main(){
int arr[] = {1, 2, 3, 4, 6, 7, 8, 5};
travles(arr, 8);//1 2 3 4 6 7 8 5
partition(arr, 0, 7);
travles(arr, 8);//1 2 3 4 5 7 8 6
return 0;
}
复制代码
接着就是快排的递归逻辑:对1 2 3 4 6 7 8 5
序列partition
以后,去除以前的比较参数5
,对剩下的子序列1234
和786
继续partition
,直到子序列为一个元素为止:
int partition(int arr[], int startIndex, int endIndex){
int small = startIndex - 1;
for (int i = startIndex; i < endIndex; ++i) {
if(arr[i] < arr[endIndex]) {
if (small + 1 != i) {
swap(arr[++small], arr[i]);
} else {
//若是small、i相邻则不用交换
small++;
}
}
}
swap(arr[++small], arr[endIndex]);
return small;
}
void quickSort(int arr[], int startIndex, int endIndex) {
if (startIndex > endIndex) {
return;
}
int index = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, index - 1);
quickSort(arr, index + 1, endIndex);
}
int main(){
int arr[] = {1, 5, 6, 2, 7, 3, 8, 0};
travles(arr, 8); //1 5 6 2 7 3 8 0
quickSort(arr, 0,7);
travles(arr, 8); //0 1 2 3 5 6 7 8
return 0;
}
复制代码
经典排序的时间复杂度与数据情况有关,若是每一次partition
时,尾元素都是序列中最大或最小的,那么去除该元素序列并未如咱们划分为样本量相同的左右两个子序列,而是只安排好了一个元素(就是去掉的那个元素),这样的话时间复杂度就是O(n-1+n-2+……+1)=O(n^2)
;但若是每一次partition
时,都将序列分红了两个样本量相差无几的左右两个子序列,那么时间复杂度就是O(nlogn)
(使用Master公式求解)。
能够发现这里partition
的过程与荷兰国旗问题中的partition
十分类似,可否之后者的partition
实现经典快排呢?咱们来试一下:
int* partition(int arr[], int startIndex, int endIndex){ ;
int small = startIndex - 1, great = endIndex + 1, i = startIndex;
while (i <= great - 1) {
if (arr[i] < arr[endIndex]) {
swap(arr[++small], arr[i++]);
} else if (arr[i] > arr[endIndex]){
swap(arr[--great], arr[i]);
} else {
i++;
}
}
int range[] = {small, great};
return range;
}
void quickSort(int arr[], int startIndex, int endIndex) {
if (startIndex > endIndex) {
return;
}
int* range = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, range[0]);
quickSort(arr, range[1], endIndex);
}
int main(){
int arr[] = {1, 5, 6, 2, 7, 3, 8, 0};
travles(arr, 8); //1 5 6 2 7 3 8 0
quickSort(arr, 0,7);
travles(arr, 8); //0 1 2 3 5 6 7 8
return 0;
}
复制代码
比较一下经典排序和使用荷兰国旗问题改进后的经典排序,不难发现,后者一次partition
能去除一个以上的元素(等于arr[endIndex]
的区域),而前者每次partition
只能去除一个元素,这里的去除至关于安排(排序)好了对应元素的位置。所以后者比经典排序更优,可是优化不大,只是常数时间内的优化,实质上的效率仍是要看数据情况(最后的状况为O(nlogn)
,最坏的状况为O(n^2)
)。
上面谈到了快排的短板是依赖数据情况,那么咱们有没有办法消除这个依赖,让他成为真正的O(nlogn)
呢?
事实上,为了让算法中的操做不依托于数据情况(如快排中每一次
partition
取尾元素做为比较,这就没有规避样本的数据情况,若是尾元素是最大或最小值就成了最坏状况)经常有两种作法:一、使用随机取数
二、将样本数据哈希打乱
随机快排就是采用上了上述第一种解决方案,在每一轮的partition
中随机选择序列中的一个数做为要比较的数:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
//产生[startIndex,endIndex]之间的随机整数
int randomInRange(int startIndex,int endIndex){
return rand() % (endIndex - startIndex + 1) + startIndex;
}
int* partition(int arr[], int startIndex, int endIndex){ ;
int small = startIndex - 1, great = endIndex + 1, i = startIndex;
int randomNum = arr[randomInRange(startIndex, endIndex)];
while (i <= great - 1) {
if (arr[i] < randomNum) {
swap(arr[++small], arr[i++]);
} else if (arr[i] > randomNum){
swap(arr[--great], arr[i]);
} else {
i++;
}
}
int range[] = {small, great};
return range;
}
void quickSort(int arr[], int startIndex, int endIndex) {
if (startIndex > endIndex) {
return;
}
int* range = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, range[0]);
quickSort(arr, range[1], endIndex);
}
void travles(int dataArr[], int length){
for (int i = 0; i < length; ++i) {
printf("%d ", dataArr[i]);
}
printf("\n");
}
int main(){
srand(time(NULL));//此后调用rand()时将以调用时的时间为随机数种子
int arr[] = {9,7,1,3,2,6,8,4,5};
travles(arr, 9);
quickSort(arr, 0,8);
travles(arr, 9);
return 0;
}
复制代码
观察比较代码能够发现随机快排只不过是在partition
时随机选出一个下标上的数做为比较对象,从而避免了每一轮选择尾元素会受数据情况的影响的问题。
那么随机快排的时间复杂度又为多少呢?
经数学论证,因为每一轮partition
选出的做为比较对象的数是随机的,即序列中的每一个数都有1/n
的几率被选上,那么该算法时间复杂度为几率事件,经数学论证该算法的数学指望为O(nlogn)
。虽说是数学指望,但在实际工程中,经常就把随机快排的时间复杂度当作O(nlog)
。
堆结构就是将一颗彻底二叉树映射到数组中的一种存储方式:
当堆的每一颗子树(包括树自己)的最大值就是其结点时称为大根堆;相反,当堆的每一颗子树的最小值就是其根结点时称为小根堆。其中大根堆的应用较为普遍,是一种很重要的数据结构。
大根堆最重要的两个操做就是heapInsert
和heapify
,前者是当一个元素加入到大根堆时应该自底向上与其父结点比较,若大于父结点则交换;后者是当堆中某个结点的数值发生变化时,应不断向下与其孩子结点中的最大值比较,若小于则交换。下面是对应的代码:
//index以前的序列符合大根堆排序,将index位置的元素加入堆结构,但不能破坏大根堆的特性
void heapInsert(int arr[],int index){
while (arr[index] > arr[(index - 1) / 2]) { //当该结点大于父结点时
swap(arr[index], arr[(index - 1) / 2]);
index = (index - 1) / 2; //继续向上比较
}
}
//数组中下标从0到heapSize符合大根堆排序
//index位置的值发生了变化,从新调整堆结构为大根堆
//heapSize指的是数组中符合大根堆排序的范围而不是数组长度,最大为数组长度,最小为0
void heapify(int arr[], int heapSize, int index){
int leftChild = index * 2 + 1;
while (leftChild < heapSize) { //当该结点有左孩子时
int greatOne = leftChild + 1 < heapSize && arr[leftChild + 1] > arr[leftChild] ?
leftChild + 1 : leftChild; //只有当右孩子存在且大于左孩子时,最大值是右孩子,不然是左孩子
greatOne = arr[greatOne] > arr[index] ? greatOne : index;//将父结点与最大孩子结点比较,肯定最大值
if (greatOne == index) {
//若是最大值是自己,则不用继续向下比较
break;
}
swap(arr[index], arr[greatOne]);
//next turn下一轮
index = greatOne;
leftChild = index * 2 + 1;
}
}
复制代码
void buildBigRootHeap(int arr[],int length){
if (arr == NULL || length <= 1) {
return;
}
for (int i = 0; i < length; ++i) {
heapInsert(arr, i);
}
}
复制代码
前面作了那么多铺垫都是为了创建大根堆,那么如何利用它来排序呢?
对应代码实现以下:
void heapSort(int arr[],int length){
if (arr == NULL || length <= 1) {
return;
}
//先创建大根堆
for (int i = 0; i < length; ++i) {
heapInsert(arr, i);
}
//循环弹出堆顶元素并heapify
int heapSize = length;
swap(arr[0], arr[--heapSize]);//至关于弹出堆顶元素
while (heapSize > 0) {
heapify(arr, heapSize, 0);
swap(arr[0], arr[--heapSize]);
}
}
int main(){
int arr[] = {9,7,1,3,6,8,4,2,5};
heapSort(arr, 9);
travles(arr, 9);
return 0;
}
复制代码
堆排序的优点在于不管是入堆一个元素heapInsert
仍是出堆一个元素以后的heapify
都不是将整个样本遍历一遍(O(n)
级别的操做),而是树层次上的遍历(O(logn)
级别的操做)。
这样的话堆排序过程当中,创建堆的时间复杂度为O(nlogn)
,循环弹出堆顶元素并heapify
的时间复杂度为O(nlogn)
,整个堆排序的时间复杂度为O(nlogn)
,额外空间复杂度为O(1)
优先级队列结构(好比Java中的
PriorityQueue
)就是堆结构。
排序算法的稳定性指的是排序先后是否维持值相同的元素在序列中的相对次序。如序列271532
,在排序过程当中若是能维持第一次出现的2
在第二次出现的2
的前面,那么该排序算法可以保证稳定性。首先咱们来分析一下前面所讲排序算法的稳定性,再来谈谈稳定性的意义。
926532
,在第一轮maxIndex
的选择出来以后(maxIndex=0
),第二次出现的2
(尾元素)将与9
交换位置,那么两个2
的相对次序就发生了变化,而这个交换是否会影响稳定性在咱们coding
的时候是不可预测的。merge
过程当中,比较大小时若是相等,那么优先插入左子序列中的数。partition
的过程会将比num
小的与small
区域的右一个数交换位置,将比num
大的与great
区域的左一个数交换位置,而small
、great
分居序列两侧,很容易打乱值相同元素的相对次序。heapify
交换的是第一层的结点和最后一层的结点。维持稳定性通常是为了知足业务需求。假设下面是一张不一样厂商下同一款产品的价格和销售状况表:
品牌 | 价格 | 销量 |
---|---|---|
三星 | 1603 | 92 |
小米 | 1603 | 74 |
vivo | 1604 | 92 |
要求先按价格排序,再按销量排序。若是保证稳定性,那么排序后应该是这样的:
品牌 | 价格 | 销量 |
---|---|---|
三星 | 1603 | 92 |
vivo | 1604 | 92 |
小米 | 1603 | 74 |
即按销量排序后,销量相同的两条记录会保持以前的按价格排序的状态,这样先前的价格排序这个工做就没白作。
以前所讲的一些算法大都是对基本类型的排序,但实际工程中要排序的对象多是没法预测的,那么如何实现一个通用的排序算法以应对呢?事实上,以前的排序均可以归类为基于比较的排序。也就是说咱们只须要对要比较的对象实现一个比较器,而后排序算法基于比较器来排序,这样算法和具体要排序的对象之间就解耦了。之后在排序以前,基于要排序的对象实现一个比较器(定义了如何比较对象大小的逻辑),而后将比较器丢给排序算法便可,这样就实现了复用。
在Java
(本人学的是Java
方向)中,这个比较器就是Comparator
接口,咱们须要实现其中的compare
方法,对于要排序的对象集合定义一个比较大小的逻辑,而后在构造用来添加这类对象的有序容器时传入这个构造器便可。封装好的容器会在容器元素发生改变时使用咱们的比较器来从新组织这些元素。
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.PriorityQueue;
import java.util.Comparator;
public class ComparatorTest {
@Data
@AllArgsConstructor
static class Student {
private long id;
private String name;
private double score;
}
static class IdAscendingComparator implements Comparator<Student> {
/** * 底层排序算法对两个元素比较时会调用这个方法 * @param o1 * @param o2 * @return 若返回正数则认为o1<o2,返回0则认为o1=o2,不然认为o1>o2 */
@Override
public int compare(Student o1, Student o2) {
return o1.getId() < o2.getId() ? -1 : 1;
}
}
public static void main(String[] args) {
//大根堆
PriorityQueue heap = new PriorityQueue(new IdAscendingComparator());
Student zhangsan = new Student(1000, "zhangsan", 50);
Student lisi = new Student(999, "lisi", 60);
Student wangwu = new Student(1001, "wangwu", 50);
heap.add(zhangsan);
heap.add(lisi);
heap.add(wangwu);
while (!heap.isEmpty()) {
System.out.println(heap.poll());//弹出并返回堆顶元素
}
}
}
复制代码
还有TreeSet
等,都是在构造是传入比较器,不然将直接根据元素的值(Java
中引用类型变量的值为地址,比较将毫无心义)来比较,这里就不一一列举了。
O(1)
,可是比较难,感兴趣的能够搜 归并排序 内部缓存法01 stable sort
(论文)arr[length-1]
或arr[randomIndex]
做为比较的标准,而这道题是将是否能整除2做为比较的标准,这类问题都同称为o1 sort
,要使这类问题作到稳定性,要看01 stable sort
这篇论文。实际工程中的排序算法通常会将 归并排序、插入排序、快速排序综合起来,集你们之所长来应对不一样的场景要求:
O(NlogN)
的优点并不明显甚至不及O(N^2)
,而在O(N^2)
的算法中,插入排序的常数时间操做最少。上一节中所讲的都是基于比较的排序,也即经过比较肯定每一个元素所处的位置。那么能不能不比较而实现排序呢?这就涉及到了 桶排序 这个方法论:准备一些桶,将序列中的元素按某些规则放入翻入对应的桶中,最后根据既定的规则依次倒出桶中的元素。
非基于比较的排序,与被排序的样本的实际数据情况有很大关系,因此在实际中并不经常使用。
计数排序是 桶排序 方法论的一种实现,即准备一个与序列中元素的数据范围大小相同的数组,而后遍历序列,将遇到的元素做为数组的下标并将该位置上的数加1。例如某序列元素值在0~100之间,请设计一个算法对其排序,要求时间复杂度为O(N)
。
#include <stdio.h>
void countSort(int arr[],int length){
int bucketArr[101];
int i;
for(i = 0 ; i <= 100 ; i++){
bucketArr[i]=0; //init buckets
}
for(i = 0 ; i < length ; i++){
bucketArr[arr[i]]++; //put into buckets
}
int count, j=0;
for(i = 0 ; i <= 100 ; i++) {
if (bucketArr[i] != 0) { //pour out
count = bucketArr[i];
while (count-- > 0) {
arr[j++] = i;
}
}
}
}
void travels(int arr[], int length){
for (int i = 0; i < length; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(){
int arr[] = {9, 2, 1, 4, 5, 2, 1, 6, 3, 8, 1, 2};
travels(arr, 12);//9 2 1 4 5 2 1 6 3 8 1 2
countSort(arr, 12);
travels(arr, 12);//1 1 1 2 2 2 3 4 5 6 8 9
return 0;
}
复制代码
若是下次面试官问你有没有事件复杂度比
O(N)
更优的排序算法时,不要忘了计数排序哦!!!
给定一个数组,求若是排序后,相邻两数的最大值,要求时间复杂度为O(N)
,且要求不能用非基于比较的排序。
这道题的思路比较巧妙:首先为这N个数准备N+1个桶,而后以其中的最小值和最大值为边界将数值范围均分红N等分,而后遍历数组将对应范围类的数放入对应的桶中,下图以数组长度为9举例
这里比较难理解的是:
对应代码以下:
#include <stdio.h>
//根据要入桶的数和最大最小值获得对应桶编号
int getBucketId(int num,int bucketsNum,int min,int max){
return (num - min) * bucketsNum / (max - min);
}
int max(int a, int b){
return a > b ? a : b;
}
int min(int a, int b){
return a < b ? a : b;
}
int getMaxGap(int arr[], int length) {
if (arr == NULL || length < 2) {
return -1;
}
int maxValue = -999999, minValue = 999999;
int i;
//找出最大最小值
for (i = 0; i < length; ++i) {
maxValue = max(maxValue, arr[i]);
minValue = min(minValue, arr[i]);
}
//记录每一个桶的最大最小值以及是否有数,初始时每一个桶都没数
int maxs[length + 1], mins[length + 1];
bool hasNum[length + 1];
for (i = 0; i < length + 1; i++) {
hasNum[i] = false;
}
//put maxValue into the last bucket
mins[length] = maxs[length] = maxValue;
hasNum[length] = true;
//iterate the arr
int bid; //bucket id
for (i = 0; i < length; i++) {
if (arr[i] != maxValue) {
bid = getBucketId(arr[i], length + 1, minValue, maxValue);
//若是桶里没数,则该数入桶后,最大最小值都是它,不然更新最大最小值
mins[bid] = !hasNum[bid] ? arr[i] : arr[i] < mins[bid] ? arr[i] : mins[bid];
maxs[bid] = !hasNum[bid] ? arr[i] : arr[i] > maxs[bid] ? arr[i] : maxs[bid];
hasNum[bid] = true;
}
}
//find the max gap between two nonEmpty buckets
int res = 0, j = 0;
for (i = 0; i < length; ++i) {
j = i + 1;//the next nonEmtpy bucket id
while (!hasNum[j]) {//the last bucket must has number
j++;
}
res = max(res, (mins[j] - maxs[i]));
}
return res;
}
int main(){
int arr[] = {13, 41, 67, 26, 55, 99, 2, 82, 39, 100};
printf("%d", getMaxGap(arr, 9)); //17
return 0;
}
复制代码
实现反转单向链表和反转双向链表的函数,要求时间复杂度为O(N)
,额外空间复杂度为O(1)
此题的难点就是反转一个结点的next指针后,就没法在该结点经过next指针找到后续的结点了。所以每次反转以前须要将该结点的后继结点记录下来。
#include<stdio.h>
#include<malloc.h>
#define MAX_SIZE 100
struct LinkNode{
int data;
LinkNode* next;
};
void init(LinkNode* &head){
head = (LinkNode*)malloc(sizeof(LinkNode));
head->next=NULL;
}
void add(int i,LinkNode* head){
LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
p->data = i;
p->next = head->next;
head->next = p;
}
void printList(LinkNode* head){
if(head==NULL)
return;
LinkNode* p = head->next;
while(p != NULL){
printf("%d ",p->data);
p = p->next;
}
printf("\n");
}
复制代码
#include<stdio.h>
#include "LinkList.cpp"
void reverseList(LinkNode *head){
if(head == NULL)
return;
LinkNode* cur = head->next;
LinkNode* pre = NULL;
LinkNode* next = NULL;
while(cur != NULL){
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
//pre -> end node
head->next = pre;
return;
}
int main(){
LinkNode* head;
init(head);
add(1,head);
add(2,head);
add(3,head);
add(4,head);
printList(head);
reverseList(head);
printList(head);
}
复制代码
请实现一个函数判断某个单链表是不是回文结构,如1->3->1
返回true
、1->2->2->1
返回true
、2->3->1
返回false
。
咱们能够利用回文链表先后两半部分逆序的特色、结合栈先进后出来求解此问题。将链表中间结点以前的结点依次压栈,而后从中间结点的后继结点开始遍历链表的后半部分,将遍历的结点与栈弹出的结点比较。
代码示例以下:
#include<stdio.h>
#include "LinkList.cpp"
#include "SqStack.cpp"
/* 判断某链表是不是回文结构 一、首先找到链表的中间结点(如果偶数个结点则是中间位置的左边一个结点) 二、使用一个栈将中间结点以前的结点压栈,而后从中间结点的后一个结点开始从栈中拿出结点比较 */
bool isPalindromeList(LinkNode* head){
if(head == NULL)
return false;
LinkNode *slow = head , *fast = head;
SqStack* stack;
init(stack);
//fast指针每走两步,slow指针才走一步
while(fast->next != NULL && fast->next->next != NULL){
fast = fast->next->next;
slow = slow->next;
push(slow,stack);
}
//链表没有结点或只有一个结点,不是回文结构
if(isEmpty(stack))
return false;
//判断偶数个结点仍是奇数个结点
if(fast->next != NULL){ //奇数个结点,slow须要再走一步
slow = slow->next;
}
//从slow的后继结点开始遍历链表,将每一个结点与栈顶结点比较
LinkNode* node;
slow = slow->next;
while(slow != NULL){
pop(stack,node);
//一旦发现有一个结点不一样就不是回文结构
if(slow->data != node->data)
return false;
slow = slow->next;
}
return true;
}
int main(){
LinkNode* head;
init(head);
add(2,head);
add(3,head);
add(3,head);
add(2,head);
printList(head);
if(isPalindromeList(head)){
printf("是回文链表");
}else{
printf("不是回文链表");
}
return 0;
}
复制代码
LinkList.cpp
:
#include<stdio.h>
#include<malloc.h>
#define MAX_SIZE 100
struct LinkNode{
int data;
LinkNode* next;
};
void init(LinkNode* &head){
head = (LinkNode*)malloc(sizeof(LinkNode));
head->next=NULL;
}
void add(int i,LinkNode* head){
LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
p->data = i;
p->next = head->next;
head->next = p;
}
void printList(LinkNode* head){
if(head==NULL)
return;
LinkNode* p = head->next;
while(p != NULL){
printf("%d ",p->data);
p = p->next;
}
printf("\n");
}
复制代码
SqStack
:
#include<stdio.h>
#include<malloc.h>
struct SqStack{
LinkNode* data[MAX_SIZE];
int length;
};
void init(SqStack* &stack){
stack = (SqStack*)malloc(sizeof(SqStack));
stack->length=0;
}
bool isEmpty(SqStack* stack){
if(stack->length > 0)
return false;
return true;
}
bool isFull(SqStack* stack){
if(stack->length == MAX_SIZE)
return true;
return false;
}
void push(LinkNode* i,SqStack* stack){
if(stack==NULL)
return;
if(!isFull(stack)){
stack->data[stack->length++] = i;
}
}
bool pop(SqStack* stack,LinkNode* &i){
if(stack==NULL)
return false;
if(!isEmpty(stack))
i = stack->data[--stack->length];
return true;
}
复制代码
进阶:要求使用时间复杂度为
O(N)
,额外空间复杂度为O(1)
求解此问题。思路:咱们能够先将链表的后半部分结点的
next
指针反向,而后从链表的两头向中间推动,逐次比较。(固然了,为了避免破坏原始数据结构,咱们在得出结论以后还须要将链表指针恢复原样)
#include<stdio.h>
#include "LinkList.cpp"
#include "SqStack.cpp"
bool isPalindromeList(LinkNode* head){
/*第一步、与方法一同样,找到中间结点*/
if(head == NULL)
return false;
LinkNode *n1 = head , *n2 = head;
while(n2->next != NULL && n2->next->next != NULL){
n2 = n2->next->next;
n1 = n1->next;
}
//若是没有结点或者只有一个首结点
if(n2 == head){
return false;
}
//若是是奇数个结点
if(n2->next != NULL){
n1 = n1->next; //n1 -> middle node
}
/*第二步、不使用额外空间,在链表自身上作文章:反转链表后半部分结点的next指针*/
n2 = n1->next; // n2 -> right part first node
n1->next = NULL;//middle node->next = NULL
LinkNode *n3 = NULL;
while (n2 != NULL) {
n3 = n2->next; //记录下一个要反转指针的结点
n2->next = n1; //反转指针
n1 = n2;
n2 = n3;
}
//n1 -> end node
n3 = n1; //record end node
n2 = head->next;
while (n2 != NULL) {
if (n2->data != n1->data) {
return false;
}
n2 = n2->next; //move n2 forward right
n1 = n1->next; //move n1 forward left
}
//recover the right part nodes
n2 = n3; //n2 -> end node
n1 = NULL;
while (n2 != NULL) {
n3 = n2->next;
n2->next = n1;
n1=n2;
n2 = n3;
}
return true;
}
/*bool isPalindromeList(LinkNode* head){ if(head == NULL) return false; LinkNode *slow = head , *fast = head; SqStack* stack; init(stack); //fast指针每走两步,slow指针才走一步 while(fast->next != NULL && fast->next->next != NULL){ fast = fast->next->next; slow = slow->next; push(slow,stack); } //链表没有结点或只有一个结点,不是回文结构 if(isEmpty(stack)) return false; //判断偶数个结点仍是奇数个结点 if(fast->next != NULL){ //奇数个结点,slow须要再走一步 slow = slow->next; } //从slow的后继结点开始遍历链表,将每一个结点与栈顶结点比较 LinkNode* node; slow = slow->next; while(slow != NULL){ pop(stack,node); //一旦发现有一个结点不一样就不是回文结构 if(slow->data != node->data) return false; slow = slow->next; } return true; }*/
int main(){
LinkNode* head;
init(head);
add(2,head);
add(3,head);
add(3,head);
add(1,head);
printList(head);
if(isPalindromeList(head)){
printf("yes");
}else{
printf("no");
}
return 0;
}
复制代码
将单向链表按某值划分红左边小、中间相等、右边大的形式
#include<stdio.h>
#include "LinkList.cpp"
/* partition一个链表有两种作法。 1,将链表中的全部结点放入一个数组中,那么就转换成了荷兰国旗问题,但这种作法会使用O(N)的额外空间; 2,分出逻辑上的small,equal,big三个区域,遍历链表结点将其添加到对应的区域中,最后再将这三个区域连起来。 这里只示范第二种作法: */
void partitionList(LinkNode *head,int val){
if(head == NULL)
return;
LinkNode *smH = NULL; //small area head node
LinkNode *smT = NULL; //small area tail node
LinkNode *midH = NULL; //equal area head node
LinkNode *midT = NULL; //equal area tail node
LinkNode *bigH = NULL; //big area head node
LinkNode *bigT = NULL; //big area tail node
LinkNode *cur = head->next;
LinkNode *next = NULL;//next node need to be distributed to the three areas
while(cur != NULL){
next = cur->next;
cur->next = NULL;
if(cur->data > val){
if(bigH == NULL){
bigH = bigT = cur;
}else{
bigT->next = cur;
bigT = cur;
}
}else if(cur->data == val){
if(midH == NULL){
midH = midT = cur;
}else{
midT->next = cur;
midT = cur;
}
}else{
if(smH == NULL){
smH = smT = cur;
}else{
smT->next = cur;
smT = cur;
}
}
cur = next;
}
//reconnect small and equal
if(smT != NULL){
smT->next = midH;
midT = midT == NULL ? midT : smT;
}
//reconnect equal and big
if(bigT != NULL){
midT->next = bigH;
}
head = smH != NULL ? smH : midH != NULL ? midH : bigH;
return;
}
int main(){
LinkNode* head;
init(head);
add(5,head);
add(2,head);
add(7,head);
add(9,head);
add(1,head);
add(3,head);
add(5,head);
printList(head);
partitionList(head,5);
printList(head);
}
复制代码
O(N)
将链表的全部结点复制一份,以key,value
为源结点,副本结点
的方式存储到哈希表中,再创建副本结点之间的关系(next、rand
指针域)
import java.util.HashMap;
import java.util.Map;
public class CopyLinkListWithRandom {
public static class Node {
public Node(int data) {
this.data = data;
}
public Node() {
}
int data;
Node next;
Node rand;
}
public static Node copyLinkListWithRandom(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Map<Node, Node> copyMap = new HashMap<>();
while (cur != null) {
copyMap.put(cur, new Node(cur.data));
cur = cur.next;
}
cur = head;
while (cur != null) {
copyMap.get(cur).next = copyMap.get(cur.next);
copyMap.get(cur).rand = copyMap.get(cur.rand);
cur = cur.next;
}
return copyMap.get(head);
}
public static void printListWithRandom(Node head) {
if (head != null) {
while (head.next != null) {
head = head.next;
System.out.print("node data:" + head.data);
if (head.rand != null) {
System.out.println(",rand data:" + head.rand.data);
} else {
System.out.println(",rand is null");
}
}
}
}
public static void main(String[] args) {
Node head = new Node();
head.next = new Node(1);
head.next.next = new Node(2);
head.next.next.next = new Node(3);
head.next.next.next.next = new Node(4);
head.next.rand = head.next.next.next.next;
head.next.next.rand = head.next.next.next;
printListWithRandom(head);
System.out.println("==========");
Node copy = copyLinkListWithRandom(head);
printListWithRandom(copy);
}
}
复制代码
O(1)
将副本结点追加到对应源结点以后,创建副本结点之间的指针域,最后将副本结点从该链表中分离出来。
//extra area O(1)
public static Node copyLinkListWithRandom2(Node head){
if (head == null) {
return null;
}
Node cur = head;
//copy every node and append
while (cur != null) {
Node copy = new Node(cur.data);
copy.next = cur.next;
cur.next = copy;
cur = cur.next.next;
}
//set the rand pointer of every copy node
Node copyHead = head.next;
cur = head;
Node curCopy = copyHead;
while (curCopy != null) {
curCopy.rand = cur.rand == null ? null : cur.rand.next;
cur = curCopy.next;
curCopy = cur == null ? null : cur.next;
}
//split
cur = head;
Node next = null;
while (cur != null) {
curCopy = cur.next;
next = cur.next.next;
curCopy.next = next == null ? null : next.next;
cur.next = next;
cur = next;
}
return copyHead;
}
复制代码
根据单链表的定义,每一个结点有且只有一个next
指针,那么若是单链表有环,它的结构将是以下所示:
相交会致使两个结点指向同一个后继结点,但不可能出现一个结点有两个后继结点的状况。
一、当相交的结点不在环上时,有以下两种状况:
二、当相交的结点在环上时,只有一种状况:
综上,两单链表若相交,要么都无环,要么都有环。
此题还须要注意的一点是若是链表有环,那么如何获取入环呢(由于不能经过
next
是否为空来判断是不是尾结点了)。这里就涉及到了一个规律:若是快指针fast
和慢指针slow
同时从头结点出发,fast
走两步而slow
走一步,当二者相遇时,将fast
指针指向头结点,使二者都一次只走一步,二者会在入环结点相遇。
public class FirstIntersectNode {
public static class Node{
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static Node getLoopNode(Node head) {
if (head == null) {
return null;
}
Node fast = head;
Node slow = head;
do {
slow = slow.next;
if (fast.next == null || fast.next.next == null) {
return null;
} else {
fast = fast.next.next;
}
} while (fast != slow);
//fast == slow
fast = head;
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
public static Node getFirstIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1); //两链表的入环结点loop1和loop2
Node loop2 = getLoopNode(head2);
//no loop
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2);
}
//both loop
if (loop1 != null && loop2 != null) {
return bothLoop(head1, head2, loop1, loop2);
}
//don't intersect
return null;
}
private static Node bothLoop(Node head1, Node head2, Node loop1, Node loop2) {
Node cur1 = head1;
Node cur2 = head2;
//入环结点相同,相交点不在环上
if (loop1 == loop2) {
int n = 0;
while (cur1.next != loop1) {
n++;
cur1 = cur1.next;
}
while (cur2.next != loop1) {
n--;
cur2 = cur2.next;
}
cur1 = n > 0 ? head1 : head2; //将cur1指向结点数较多的链表
cur2 = cur1 == head1 ? head2 : head1; //将cur2指向另外一个链表
n = Math.abs(n);
while (n != 0) { //将cur1先走两链表结点数差值个结点
cur1 = cur1.next;
n--;
}
while (cur1 != cur2) { //cur1和cur2会在入环结点相遇
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
//入环结点不一样,相交点在环上
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) { //链表2的入环结点在链表1的环上,说明相交
return loop1; //返回loop1或loop2都可,由于整个环就是两链表的相交部分
}
cur1 = cur1.next;
}
//在链表1的环上转了一圈也没有找到链表2的入环结点,说明不想交
return null;
}
private static Node noLoop(Node head1, Node head2) {
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
while (cur1.next != null) {
n++;
cur1 = cur1.next;
}
while (cur2.next != null) {
n--;
cur2 = cur2.next;
}
if (cur1 != cur2) { //两链表的尾结点不一样,不可能相交
return null;
}
cur1 = n > 0 ? head1 : head2; //将cur1指向结点数较多的链表
cur2 = cur1 == head1 ? head2 : head1; //将cur2指向另外一个链表
n = Math.abs(n);
while (n != 0) { //将cur1先走两链表结点数差值个结点
cur1 = cur1.next;
n--;
}
while (cur1 != cur2) { //cur1和cur2会在入环结点相遇
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
public static void printList(Node head) {
for (int i = 0; i < 50; i++) {
System.out.print(head.data+" ");
head = head.next;
}
System.out.println();
}
}
复制代码
对应三种状况测试以下:
public static void main(String[] args) {
//==================== both loop ======================
//1->2->[3]->4->5->6->7->[3]...
Node head1 = new Node(1);
head1.next = new Node(2);
head1.next.next = new Node(3);
head1.next.next.next = new Node(4);
head1.next.next.next.next = new Node(5);
head1.next.next.next.next.next = new Node(6);
head1.next.next.next.next.next.next = new Node(7);
head1.next.next.next.next.next.next.next = head1.next.next;
//9->8->[6]->7->3->4->5->[6]...
Node head2 = new Node(9);
head2.next = new Node(8);
head2.next.next = head1.next.next.next.next.next;
head2.next.next.next = head1.next.next.next.next.next.next;
head2.next.next.next.next = head1.next.next;
head2.next.next.next.next.next = head1.next.next.next;
head2.next.next.next.next.next.next = head1.next.next.next.next;
head2.next.next.next.next.next.next.next = head1.next.next.next.next.next;
printList(head1);
printList(head2);
System.out.println(getFirstIntersectNode(head1, head2).data);
System.out.println("==================");
//1->[2]->3->4->5->6->7->8->4...
Node head3 = new Node(1);
head3.next = new Node(2);
head3.next.next = new Node(3);
head3.next.next.next = new Node(4);
head3.next.next.next.next = new Node(5);
head3.next.next.next.next.next = new Node(6);
head3.next.next.next.next.next.next = new Node(7);
head3.next.next.next.next.next.next.next = new Node(8);
head3.next.next.next.next.next.next.next.next = head1.next.next.next;
//9->0->[2]->3->4->5->6->7->8->4...
Node head4 = new Node(9);
head4.next = new Node(0);
head4.next.next = head3.next;
head4.next.next.next = head3.next.next;
head4.next.next.next.next = head3.next.next.next;
head4.next.next.next.next.next = head3.next.next.next.next;
head4.next.next.next.next.next.next = head3.next.next.next.next.next;
head4.next.next.next.next.next.next.next = head3.next.next.next.next.next.next;
head4.next.next.next.next.next.next.next.next = head3.next.next.next.next.next.next.next;
head4.next.next.next.next.next.next.next.next.next = head3.next.next.next;
printList(head3);
printList(head4);
System.out.println(getFirstIntersectNode(head3,head4).data);
System.out.println("==================");
//============= no loop ==============
//1->[2]->3->4->5
Node head5 = new Node(1);
head5.next = new Node(2);
head5.next.next = new Node(3);
head5.next.next.next = new Node(4);
head5.next.next.next.next = new Node(5);
//6->[2]->3->4->5
Node head6 = new Node(6);
head6.next = head5.next;
head6.next.next = head5.next.next;
head6.next.next.next = head5.next.next.next;
head6.next.next.next.next = head5.next.next.next.next;
System.out.println(getFirstIntersectNode(head5,head6).data);
}
复制代码
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#define MAX_SIZE 1000
struct ArrayStack{
int data[MAX_SIZE];
int top;
};
void init(ArrayStack *&stack) {
stack = (ArrayStack *) malloc(sizeof(ArrayStack));
stack->top = -1;
}
bool isEmpty(ArrayStack* stack){
return stack->top == -1 ?;
}
bool isFull(ArrayStack *stack){
return stack->top == MAX_SIZE - 1 ?;
}
void push(int i, ArrayStack *stack){
if (!isFull(stack)) {
stack->data[++stack->top] = i;
}
}
int pop(ArrayStack* stack){
if (!isEmpty(stack)) {
return stack->data[stack->top--];
}
}
int getTopElement(ArrayStack *stack){
if (!isEmpty(stack)) {
return stack->data[stack->top];
}
}
int main(){
ArrayStack* stack;
init(stack);
push(1, stack);
push(2, stack);
push(3, stack);
printf("%d ", pop(stack));
printf("%d ", getTopElement(stack));
printf("%d ", pop(stack));
printf("%d ", pop(stack));
//3 2 2 1
return 0;
}
复制代码
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#define MAX_SIZE 1000
//数组结构实现的环形队列
struct ArrayCircleQueue{
int data[MAX_SIZE];
int front,rear;
};
void init(ArrayCircleQueue *&queue){
queue = (ArrayCircleQueue *) malloc(sizeof(ArrayCircleQueue));
queue->front = queue->rear = 0;
}
bool isEmpty(ArrayCircleQueue *queue){
return queue->front == queue->rear;
}
bool isFull(ArrayCircleQueue *queue){
return (queue->rear+1)%MAX_SIZE==queue->front;
}
void enQueue(int i, ArrayCircleQueue *queue){
if (!isFull(queue)) {
//move the rear and fill it
queue->data[++queue->rear] = i;
}
}
int deQueue(ArrayCircleQueue *queue){
if (!isEmpty(queue)) {
return queue->data[++queue->front];
}
}
int main(){
ArrayCircleQueue* queue;
init(queue);
enQueue(1, queue);
enQueue(2, queue);
enQueue(3, queue);
while (!isEmpty(queue)) {
printf("%d ", deQueue(queue));
}
}
复制代码
实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操做getMin
。要求以下:
pop
、push
、getMin
操做的时间复杂度都是O(1)
。思路:因为每次
push
以后都会可能致使栈中已有元素的最小值发生变化,所以须要一个容器与该栈联动(记录每次push
产生的栈中最小值)。咱们能够借助一个辅助栈,数据栈push
第一个元素时,将其也push
到辅助栈,此后每次向数据栈push
元素的同时将其和辅助栈的栈顶元素比较,若是小,则将其也push
到辅助栈,不然取辅助栈的栈顶元素push
到辅助栈。(数据栈正常push
、pop
数据,而辅助栈push
每次数据栈push
后产生的栈中最小值;但数据栈pop
时,辅助栈也只需简单的pop
便可,保持同步)
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#include "ArrayStack.cpp"
int min(int a, int b){
return a < b ? a : b;
}
struct GetMinStack{
ArrayStack* dataStack;
ArrayStack* helpStack;
};
void initGetMinStack(GetMinStack* &stack){
stack = (GetMinStack *) malloc(sizeof(GetMinStack));
init(stack->dataStack);
init(stack->helpStack);
}
void push(int i, GetMinStack *stack) {
if (!isFull(stack->dataStack)) {
push(i, stack->dataStack); //ArrayStack.cpp
if (!isEmpty(stack->helpStack)) {
i = min(i, getTopElement(stack->helpStack));
}
push(i, stack->helpStack);
}
}
int pop(GetMinStack* stack){
if (!isEmpty(stack->dataStack)) {
pop(stack->helpStack);
return pop(stack->dataStack);
}
}
int getMin(GetMinStack *stack){
if (!isEmpty(stack->dataStack)) {
return getTopElement(stack->helpStack);
}
}
int main(){
GetMinStack *stack;
initGetMinStack(stack);
push(6, stack);
printf("%d ", getMin(stack));//6
push(3, stack);
printf("%d ", getMin(stack));//3
push(1, stack);
printf("%d ", getMin(stack));//1
pop(stack);
printf("%d ", getMin(stack));//3
return 0;
}
复制代码
思路:只要将关注点放在 后进先出 这个特性就不难实现了。使用一个数据队列和辅助队列,当放入数据时使用队列的操做正常向数据队列中放,但出队元素时,需将数据队列的前n-1个数入队辅助队列,而将数据队列的队尾元素弹出来,最后数据队列和辅助队列交换角色。
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#include "../queue/ArrayCircleQueue.cpp"
struct DoubleQueueStack{
ArrayCircleQueue* dataQ;
ArrayCircleQueue* helpQ;
};
void init(DoubleQueueStack* &stack){
stack = (DoubleQueueStack *) malloc(sizeof(DoubleQueueStack));
init(stack->dataQ);
init(stack->helpQ);
}
void swap(ArrayCircleQueue *&dataQ, ArrayCircleQueue *&helpQ){
ArrayCircleQueue* temp = dataQ;
dataQ = helpQ;
helpQ = temp;
}
void push(int i,DoubleQueueStack* stack){
if (!isFull(stack->dataQ)) {
return enQueue(i, stack->dataQ);
}
}
int pop(DoubleQueueStack* stack){
if (!isEmpty(stack->dataQ)) {
int i = deQueue(stack->dataQ);
while (!isEmpty(stack->dataQ)) {
enQueue(i, stack->helpQ);
i = deQueue(stack->dataQ);
}
swap(stack->dataQ, stack->helpQ);
return i;
}
}
bool isEmpty(DoubleQueueStack* stack){
return isEmpty(stack->dataQ);
}
int getTopElement(DoubleQueueStack* stack){
if (!isEmpty(stack->dataQ)) {
int i = deQueue(stack->dataQ);
while (!isEmpty(stack->dataQ)) {
enQueue(i, stack->helpQ);
i = deQueue(stack->dataQ);
}
enQueue(i, stack->helpQ);
swap(stack->dataQ, stack->helpQ);
return i;
}
}
int main(){
DoubleQueueStack *stack;
init(stack);
push(1, stack);
push(2, stack);
push(3, stack);
while (!isEmpty(stack)) {
printf("%d ", pop(stack));
}
push(4, stack);
printf("%d ", getTopElement(stack));
return 0;
}
复制代码
思路:使用两个栈,一个栈
PutStack
用来放数据,一个栈GetStack
用来取数据。取数据时,若是PulllStack
为空则须要将PutStack
中的全部元素一次性依次pop
并放入GetStack
。特别要注意的是这个 倒数据的时机:
- 只有当
GetStack
为空时才能往里倒- 倒数据时必须一次性将
PutStack
中的数据倒完
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#include "../stack/ArrayStack.cpp"
struct DoubleStackQueue{
ArrayStack* putStack;
ArrayStack* getStack;
};
void init(DoubleStackQueue *&queue){
queue = (DoubleStackQueue *) malloc(sizeof(DoubleStackQueue));
init(queue->putStack);
init(queue->getStack);
}
bool isEmpty(DoubleStackQueue *queue){
return isEmpty(queue->getStack) && isEmpty(queue->putStack);
}
void pour(ArrayStack *stack1, ArrayStack *stack2){
while (!isEmpty(stack1)) {
push(pop(stack1), stack2);
}
}
void enQueue(int i, DoubleStackQueue *queue){
if (!isFull(queue->putStack)) {
push(i, queue->putStack);
} else {
if (isEmpty(queue->getStack)) {
pour(queue->putStack, queue->getStack);
push(i, queue->putStack);
}
}
}
int deQueue(DoubleStackQueue* queue){
if (!isEmpty(queue->getStack)) {
return pop(queue->getStack);
} else {
if (!isEmpty(queue->putStack)) {
pour(queue->putStack, queue->getStack);
return pop(queue->getStack);
}
}
}
int main(){
DoubleStackQueue *queue;
init(queue);
enQueue(1, queue);
printf("%d\n", deQueue(queue));
enQueue(2, queue);
enQueue(3, queue);
while (!isEmpty(queue)) {
printf("%d ", deQueue(queue));
}
return 0;
}
复制代码