数据结构和算法 - 基础笔记梳理

380 阅读18分钟

备战秋招,复习基础。如有错误,欢迎批评指正,共同进步!

数据结构

数据结构 查找 插入 删除 遍历
数组 O(n) O(1) O(n) -
有序数组 O(logn) O(n) O(n) O(n)
链表 O(n) O(1) O(n) -
有序链表 O(n) O(n) O(n) O(n)
二叉树(一般情况) O(logn) O(logn) O(logn) O(n)
二叉树(最坏情况) O(n) O(n) O(n) O(n)
平衡树(一般+最坏) O(logn) O(logn) O(logn) O(n)
哈希表 O(1) O(1) O(1) -

二叉树遍历

前序遍历:根 左 右 中序遍历:左 根 右 后序遍历:左 右 根

赫夫曼树

最优二叉树,带权路径长度(树根到每一个节点的路径长度与节点的权的乘积的和)最小 构造:

1 给定n个带权值的节点,集合为F
2 从F中选取两颗根节点权值最小的树,作为左右子树,构造新的二叉树,新根节点的权值为左右子树根节点权值之和
3 从F中删除两棵子树,同时将新二叉树加入F
4 重复2和3,直到只剩一棵树。

哈希

在纪录的存储位置和它的关键字之间建立一个对应关系f,使每个关键字和结构中一个唯一的存储位置与之对应。这个对应关系称为哈希函数。

解决冲突的办法

1 开放定址法:H(key)+di ← 增量序列,线性再探测/二次探测/随机探测
2 再哈希法:计算另一个哈希函数的地址,但会增加计算时间
3 链地址法:所有关键字为同义词的纪录存储在同一个线性链表中
4 建立公共溢出区

堆是具有以下性质的完全二叉树:

  • 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
  • 或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

C语言实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define maxn 110//栈的最大值
typedef int elem;    //方便修改数据类型
typedef struct{
    int top;    ///栈顶的索引
    elem index[maxn];
}Stack;
Stack stack;   ///为了方便这里直接定义全局变量了,用局部变量的话在每个函数加上取地址符和声明就行了
void stack_pop(){   ///元素出栈,此函数无返回值
    stack.top--;
}
void stack_push(elem buf){  ///元素入栈
    stack.index[++stack.top] = buf;
}
int stack_empty(){///判空,如果栈为空的话返回1,否则返回0
    return stack.top == -1;
}
 
void stack_clear(){ ///清空栈
    stack.top = -1;
}
 
int stack_size(){   ///求栈内元素数
    return stack.top+1;
}
elem stack_top(){   ///返回栈顶元素
    return stack.index[stack.top];
}
 
int main()
{
    stack_clear();///初始化栈
    elem buf;
    buf = 10;
    stack_push(buf);
    printf("%d\n",stack_top());
    printf("%d\n",stack_empty());
    printf("%d\n",stack_size());
    stack_pop();
    printf("%d\n",stack_size());
    return 0;
}

常用算法

排序

方法 平均时间 最坏情况 最好情况 稳定度 额外空间 备注
直接插入 O(n^2) O(n^2) O(n) 稳定 O(1) 大部分已排序时较好
希尔 O(nlogn) O(nlogn) 与步长相关 不稳定 O(1) n小时较好
冒泡 O(n^2) O(n^2) O(n) 稳定 O(1) n小时较好
快排 O(nlogn) O(n^2) O(nlog2n) 不稳定 O(log2n) n大时较好,基本有序时反而不好
直接选择 O(n^2) O(n^2) O(n^2) 不稳定 O(1) n小时较好
二叉树排序 O(nlog2n) O(n^2) 不一定 O(1)
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) 不稳定 O(1) n大时较好
归并 O(nlogn) O(nlogn) O(nlogn) 稳定 O(1) n大时较好
基数 O(d(n+r)) O(d(n+r)) O(d(n+r)) 稳定 O(r) d为位数,r为基数
计数 O(n+k) O(n+k) O(n+k) 稳定 O(n+k) 优于比较排序,0~k为数值范围
桶排序 O(n+c) O(nlogn)所有元素落到一个桶中 O(n) 稳定 O(n+m) n为数的个数,m为桶数,c=n*(logn-logm)桶越多效率越高。当n=m时达到O(n)但占用空间大。桶内可用快排

快速排序

1 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3 从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;
4 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;
5 重复第3、4步,直到i=j; 

参考:快速排序算法javascript实现

/**
题目:快速排序算法
思路:两个哨兵,i,j,j从右边找比基数小的,i从左边找比基数大的,然后交换两个目标元素的位置,直到i=j,然后交换i和基数的位置,递归处理。
**/
function quick_sort(arr,from,to){
	var i = from; //哨兵i
	var j = to; //哨兵j
	var key = arr[from]; //标准值
	if(from >= to){ //如果数组只有一个元素
	   return;
	}
	while(i < j){
		while(arr[j] > key && i < j){ //从右边向左找第一个比key小的数,找到或者两个哨兵相碰,跳出循环
			j--;
		}
		while(arr[i] <= key && i < j){  //从左边向右找第一个比key大的数,找到或者两个哨兵相碰,跳出循环,这里的=号保证在本轮循环结束前,key的位置不变,否则的话跳出循环,交换i和from的位置的时候,from位置的上元素有可能不是key
			i++;
		}
		/**
		  代码执行道这里,1、两个哨兵到找到了目标值。2、j哨兵找到了目标值。3、两个哨兵都没找到(key是当前数组最小值)
		**/
		if(i < j){ //交换两个元素的位置
			var temp = arr[i];
			arr[i] = arr[j];
			arr[j] = temp;
 
		}
	}
	arr[from] = arr[i] //
	arr[i] = key;
    quick_sort(arr,from,i-1);
	quick_sort(arr,i+1,to);
}
 
var arr = [3,3,-5,6,0,2,-1,-1,3];
console.log(arr);
quick_sort(arr,0,arr.length-1);
console.log(arr);

C语言: 参考:C语言实现----快速排序

#include<stdio.h>
void Swap(int arr[], int low, int high)
{
    int temp;
    temp = arr[low];
    arr[low] = arr[high];
    arr[high] = temp;
}
 
int Partition(int arr[], int low, int high)
{
    int base = arr[low];
    while(low < high)
    {
        while(low < high && arr[high] >= base)
        {
            high --;
        }
        Swap(arr, low, high);
        while(low < high && arr[low] <= base)
        {
            low ++;
        }
        Swap(arr, low, high);
    }
    return low;
}
 
void QuickSort(int arr[], int low, int high)
{
    if(low < high)
    {
        int base = Partition(arr, low, high);
        QuickSort(arr, low, base - 1);
        QuickSort(arr, base + 1, high);
    }
}
 
int main()
{
    int n;
    scanf("%d\n",&n);
    int arr[n];
    int i , j;
    for(i = 0; i < n; i ++)
    {
        scanf("%d",&arr[i]);
    }
    printf("\n");
    QuickSort(arr, 0, n-1);
    for(j = 0; j < n; j ++)
    {
        printf("%4d",arr[j]);
    }
    return 0;
}

冒泡排序

1 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3 针对所有的元素重复以上的步骤,除了最后一个。
4 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
function bSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len-1; i++) {
      for (var j = 0; j < len - 1 - i; j++) {
           // 相邻元素两两对比,元素交换,大的元素交换到后面
          if (arr[j] > arr[j + 1]) {
              var temp = arr[j];
              arr[j] = arr[j+1];
              arr[j+1] = temp;
          }
      }
    }
    return arr;
  }
  
  //举个数组
  myArr = [20,18,27,19,35];
  //使用函数
  bSort(myArr)

C语言: 参考:八种基本的排序(1)——冒泡排序(C语言实现)

//冒泡排序,从小到大(方向可改) 。
#include <stdio.h> 
void bubble_sort(int a[], int n);   //申明函数"bubble_sort" 
int number[10000000];   //在主函数外面定义数组可以更长 
void bubble_sort(int a[], int n)    //下面是函数bubble_sort的程序 
{
    int i,j,temp;    //定义三个整型变量 
    for (j=0;j<n-1;j++)    //用一个嵌套循环来遍历一遍每一对相邻元素 (所以冒泡函数慢嘛,时间复杂度高)  
    {                           
        for (i=0;i<n-1-j;i++)
        {
            if(a[i]>a[i+1])  //从大到小排就把左边的">"改为"<" !!!
            {
                temp=a[i];      //a[i]与a[i+1](即a[i]后面那个) 交换
                a[i]=a[i+1];    //基本的交换原理"c=a;a=b;b=c" 
                a[i+1]=temp;
            }
        }
    }    
}

int main()      //主函数 
{
    int i,n;
    printf("输入数字个数:\n");    
    scanf("%d",&n);      //输入数字个数
    printf("输入%d个数:\n",n);
    for(int j=0;j<n;j++)    //用一个for循环来输入所有数字 
        scanf("%d",&number[j]) ;
    bubble_sort(number,n);   //引用函数bubble_sort 
    for (i=0;i<n-1;i++)   //输出传来的排序好的数组 
        printf("%d ",number[i]);   //这里这么写是因为有些题有格式要求(最后一个数后面不能有空格)                                
    printf("%d\n",number[i]);
    return 0;
}
//ENDING

归并排序

1 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2 设定两个指针,最初位置分别为两个已经排序序列的起始位置
3 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4 重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾
// 融合两个有序数组,这里实际上是将数组 arr 分为两个数组
function mergeArray(arr, first, mid, last, temp) {
    let i = first; 
    let m = mid;
    let j = mid+1;
    let n = last;
    let k = 0;
    while(i<=m && j<=n) {
      if(arr[i] < arr[j]) {
        temp[k++] = arr[i++];
      } else {
        temp[k++] = arr[j++];
      }
    }
    while(i<=m) {
      temp[k++] = arr[i++];
    }
    while(j<=n) {
      temp[k++] = arr[j++];
    } 
    for(let l=0; l<k; l++) {
      arr[first+l] = temp[l];
    }
    return arr;
  }
  // 递归实现归并排序
  function mergeSort(arr, first, last, temp) {
    if(first<last) {
      let mid = Math.floor((first+last)/2);
      mergeSort(arr, first, mid, temp);    // 左子数组有序
      mergeSort(arr, mid+1, last, temp);   // 右子数组有序
      arr = mergeArray(arr, first, mid, last, temp);  
    }
    return arr;
  }
  
  // example
  let arr = [10, 3, 1, 5, 11, 2, 0, 6, 3];
  let temp = new Array();
  let SortedArr = mergeSort(arr, 0 ,arr.length-1, temp);
  alert(SortedArr);

C语言: 参考:八种基本的排序(4)——归并排序(C语言实现)

//归并排序(从小到大) 
#include <stdio.h>
int a[3001000];   //在主函数外定义数组 
int c[3001000];
void merge_sort(int left,int right)   //定义归并函数"merge_sort" 
{
    if ( left == right ) return;   //判断是否只有一个数 
    int mid = ( left + right ) / 2;   //取一个中间数 
    merge_sort(left, mid);      //这两行将输入的数列强行二分 
    merge_sort(mid + 1,right);
    int i = left;   //把开始和中间的值保存在别的变量中 
    int j = mid + 1;
    int len = 0;
    while (i <= mid && j <= right)    //在范围内判断前后两数的大小 
    {
        if (a[i] < a[j])    //判断大小 大到小"<",小到大">"!!! 
        {
            c[len] = a[i];    //如果条件成立(这里是后数比前数小)把后面的值赋到前面 
            len++;   //表示判断过一遍 
            i++;    //当i与下面的j其中有一个不满足上面while后的条件则跳出循环,表示排序完成 
        }
        else
        {
            c[len] = a[j];  //不成立就不变 
            len++;
            j++;
        }
    }
    for (;i<=mid;i++)    //下面几个for循环把排序好的数记录下来 
    {
        c[len] = a[i];      
        len++;           //挨个赋值 
    }
    for (;j<=right;j++)
    {
        c[len] = a[j];
        len++;
    }
    for (int ii = left; ii <= right ;ii++)
        a[ii] = c[ii - left];
}


int main()    //主函数 
{
    int n;
    printf("输入数字个数:\n");
    scanf("%d",&n);   //输入要排序的数字个数 
    printf("输入%d个数:\n",n);
    for (int i = 0 ; i < n ; i++)   //循环输入 
        scanf("%d",&a[i]);
    merge_sort(0,n-1);   //调用归并排序函数"merge_sort" 
    for (int i = 0 ; i < n ; i++)   //循环输出
    {
        if(i!=0)     //第一个数前面不加空格 
        printf(" ");
        printf("%d",a[i]);
    }
    return 0;
}
//ENDING

堆排序

1 初始化堆:将数列a[1...n]构造成最大堆。
2 交换数据:将a[1]和a[n]交换,使a[n]是a[1...n]中的最大值;然后将a[1...n-1]重新调整为最大堆。 
3 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1...n-1]中的最大值;然后将a[1...n-2]重新调整为最大值。 
4 依次类推,直到整个数列都是有序的。
// 交换两个节点
function swap(A, i, j) {
    let temp = A[i];
    A[i] = A[j];
    A[j] = temp; 
  }
  
  // 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
  // 假设 结点 i 以下的子堆已经是一个大顶堆,shiftDown函数实现的
  // 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。后面
  // 将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
  // 都执行 shiftDown操作,所以就满足了结点 i 以下的子堆已经是一大
  //顶堆
  function shiftDown(A, i, length) {
    let temp = A[i]; // 当前父节点
  // j<length 的目的是对结点 i 以下的结点全部做顺序调整
    for(let j = 2*i+1; j<length; j = 2*j+1) {
      temp = A[i];  // 将 A[i] 取出,整个过程相当于找到 A[i] 应处于的位置
      if(j+1 < length && A[j] < A[j+1]) { 
        j++;   // 找到两个孩子中较大的一个,再与父节点比较
      }
      if(temp < A[j]) {
        swap(A, i, j) // 如果父节点小于子节点:交换;否则跳出
        i = j;  // 交换后,temp 的下标变为 j
      } else {
        break;
      }
    }
  }
  
  // 堆排序
  function heapSort(A) {
    // 初始化大顶堆,从第一个非叶子结点开始
    for(let i = Math.floor(A.length/2-1); i>=0; i--) {
      shiftDown(A, i, A.length);
    }
    // 排序,每一次for循环找出一个当前最大值,数组长度减一
    for(let i = Math.floor(A.length-1); i>0; i--) {
      swap(A, 0, i); // 根节点与最后一个节点交换
      shiftDown(A, 0, i); // 从根节点开始调整,并且最后一个结点已经为当
                           // 前最大值,不需要再参与比较,所以第三个参数
                           // 为 i,即比较到最后一个结点前一个即可
    }
  }
  
  let Arr = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
  heapSort(Arr);
  alert(Arr);

插入排序

function insertSort(arr) {
    var len = arr.length;
    var temp;
    for (var i = 1;i < len;i++){
        temp = arr[i]
        for (var j = i;j > 0 && temp < arr[j-1];j--){
            // 当前值和之前的每个值进行比较,发现有比当前值小的值就进行重新赋值
            arr[j] = arr[j-1];
        }
        arr[j] = temp;
    }
    return arr;
}

C语言: 参考:八种基本的排序(3)——插入排序(C语言实现)

//插入排序(从小到大) 
#include<stdio.h>
int number[100000000];     //在外面定义数组 
void insertion_sort(int *number,int n)    //定义一个插入函数"insertion_sort" 
{
    int i=0,ii=0,temp=0;  
    for(i=1;i<n;i++)  //循环遍历 
    {
        temp=number[i];  //将temp每一次赋值为number[i] 
        ii=i-1;  
        while(ii>=0&&temp<number[ii])   //这里改顺序 (temp后的)"<"为小到大,">"为大到小 !!!
        {
            number[ii+1]=number[ii];    //将大的元素往前放 
            ii--; 
        }
        number[ii+1]=temp;   //与"number[ii+1]=number[ii];"一起意为 
    }              //如果插入的数比之前的大,将number[ii]与number[ii+1]互换 
}
int main() 
{
    int i=0,n;
    printf("输入数字个数:\n");    
    scanf("%d",&n);       //输入要排序的数字的个数 
    printf("输入%d个数:\n",n);
    for(int j=0;j<n;j++)       //将所有数全放入number数组中 
        scanf("%d",&number[j]) ;
    insertion_sort(number,n);   //引用插入函数 
    for(i=0;i<n-1;i++)    //循环输出 
        printf("%d ",number[i]);    //格式需要  
    printf("%d\n",number[i]);
    return 0;
}
//ENDING

选择排序

var arNums = [65, 52, 74, 14, -1, 2, 88];
var b = 0;
for (var i = 0; i < arNums.length; i++) {
    for (var j = i+1; j < arNums.length; j++) {
        if (arNums[i] > arNums[j]) {
            a = arNums[i];
            arNums[i] = arNums[j];
            arNums[j] = a;
        }
    }
}
for (var i in arNums) {
    console.log(i)
    if (i != arNums.length - 1) {
        document.write(arNums[i] + ',');
    } else {
        document.write(arNums[i]);
    }
}

C语言: 参考:八种基本的排序(2)——直接选择排序(C语言实现)

//选择排序(从小到大排)
#include<stdio.h>
int number[100000000];    //在主函数外定义数组可以更长多了 
void select_sort(int R[],int n)    //定义选择排序函数"select_sort" 
{
    int i,j,k,index;    //定义变量 
    for(i=0;i<n-1;i++)   //遍历 
    {
        k=i;
        for(j=i+1;j<n;j++)    //j初始不为0,冒泡初始为0,所以选排比冒泡快,但不稳定 
        {
            if(R[j]<R[k])   //顺序从这里改顺序 小到大"<",大到小">" !!!
                k=j;      //这里是区分冒泡排序与选择排序的地方,冒泡没这句 
        }
        if(k!=j)    //为了严谨,去掉也行 
        {
            index=R[i];   //交换R[i]与R[k]中的数 
            R[i]=R[k];    //简单的交换c=a,a=b,b=c 
            R[k]=index;
        }
    }
} 

int main()     //主程序 
{
    int i,n;
    printf("输入数字个数:\n");    
    scanf("%d",&n);     //输入要排序的数字的个数 
    printf("输入%d个数:\n",n);
    for(int j=0;j<n;j++)     //将所有数全放入number数组中 
        scanf("%d",&number[j]) ;
    select_sort(number,n);   //引用选择排序select_sort的函数 
    for (i=0;i<n-1;i++)    //用for循环输出排完排完序的数组 
        printf("%d ",number[i]);   //这样写是为了格式(最后一个数后面不能有空格)                                  
    printf("%d\n",number[i]);
    return 0;   //好习惯 
}
//ENDING

查找

二分查找

function binary_search(arr,low, high, key) {
    if (low > high){
        return -1;
    }
    var mid = parseInt((high + low) / 2);
    if(arr[mid] == key){
        return mid;
    }else if (arr[mid] > key){
        high = mid - 1;
        return binary_search(arr, low, high, key);
    }else if (arr[mid] < key){
        low = mid + 1;
        return binary_search(arr, low, high, key);
    }
};
var arr = [1,2,3,4,5,6,7,8,9,10,11,23,44,86];
var result = binary_search(arr, 0, 13, 10);
alert(result); // 9 返回目标元素的索引值  

五大算法

贪心算法

局部最优算法,每一步都取当前最优。

分治算法

分成多个小模块,子问题相互独立。

动态规划

每个状态都是过去历史的一个总结。

回溯法

发现原先选择不优时,返回重新选择。

分支限界法

求解整数规划。分割子集、计算下界、剪枝超界部分

常考题纪录

大数相加相减相乘

参考资料:大数的四则运算(加法、减法、乘法、除法)

相加:两个大数用数组保存,在数组中逐位进行相加,再判断该位相加后是否需要进位。为了方便计算,将数字的低位放在数组的前面,高位放在后面。

相减:从低位开始减。先判断被减数和减数哪一个大,决定是否加负号;然后处理每一项,如果前一位相减有借位,就先减去上一位的借位,无则不减,再去判断是否能够减开被减数,如果减不开,就要借位后再去减,同时置借位为1,否则置借位为0。

相乘:一个数的第i 位和另一个数的第j 位相乘所得的数,一定是要累加到结果的第i+j 位上。这里i, j 都是从右往左,从0 开始数。ans[i+j] = a[i]*b[j];

相除:基本思想是反复做减法,看从被除数里面最多能减去多少个除数,商就是多少。一次最多能减少多少个除数的10的n次方。

以7546除以23为例:
先用7546减去23的100倍,即减去2300,可以减3次,余下646,此时商就是300 (300=100*3);
然后646减去23的10倍,即减去230,可以减2次,余下186,此时商就是320 (320=300+10*2);
然后186减去23,可以减8次,余下2,此时商就是328 (328=320+1*8);
因为2除以23的结果小于1,而我们又不用计算小数点位,所以不必再继续算下去了。

约瑟夫环

参考资料:秒懂约瑟夫环

约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解。

括号匹配

参考资料:LeetCode-32.最长有效括号(考察点:栈/动态规划)

解题思路:

1. 需有一个变量start记录有效括号子串的起始下标,max表示最长有效括号子串长度,初始值均为0

2. 遍历给字符串中的所有字符

    2.1. 若当前字符s[index]为左括号'(',将当前字符下标index入栈(下标稍后有其他用处),处理下一字符
 
    2.2 若当前字符s[index]为右括号')',判断当前栈是否为空

        2.2.1 若栈为空,则start = index + 1,处理下一字符(当前字符右括号下标不入栈)
 
        2.2.2 若栈不为空,则出栈(由于仅左括号入栈,则出栈元素对应的字符一定为左括号,可与当前字符右括号配对),判断栈是否为空
 
            2.2.2.1 若栈为空,则max = max(max, index-start+1)
 
            2.2.2.2 若栈不为空,则max = max(max, index-栈顶元素值)

大数据题

资料参考:大数据量的算法面试题