刷题总结

目录

 

  这是自己的刷题总结,题目主要包含:剑指offer、程序员代码面试指南、LeetCode,对于一些解题方法和思路除却上面三块外,还有在网上找的各种比较好的解决方法。

 

数据结构总览

  数据结构有散列表、栈、队列、堆、树、图等等各种数据结构,但归根结底这些数据结构只有两种存储方式,数组(顺序存储)和链表(链式存储)。

  队列、栈这两种数据结构既可以使链表也可以使用数组实现。数组实现,就要处理扩容缩容的问题;链表实现,没有这个问题,但需要更多的内存空间存储节点指针。

  图的两种表示方法法,邻接表就是链表,邻接矩阵就是二维数组。邻接矩阵判断连通性迅速,并且可以进矩阵运算解决一些问题,但是如果图比较稀疏的话很耗费空间。邻接表比较节省空间,但是很多操作的效率上肯定比不过邻接矩阵。

  散列表就是通过散列函数把键映射到一个大数组里。而且对于解决散列冲突的方法,拉链法需要链表特性,操作简单,但需要额外的空间存储指针;线性探查法就需要数组特性,以便连续寻址,不需要指针的存储空间,但操作稍微复杂些。

  树,用数组实现就是堆,因为堆是一个完全二叉树,用数组存储不需要节点指针,操作也比较简单;用链表实现就是很常见的那种树,因为不一定是完全二叉树,所以不适合用数组存储。为此,在这种链表树结构之上,又衍生出各种巧妙的设计,比如二叉搜索树、AVL树、红黑树、区间树、B 树等等,以应对不同的问题。

  数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,并且相对节约存储空间。但正因为连续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度 O(N);并且你如果想在数组中间进行插入和删除,每次必须搬移后面的所有数据以保持连续,时间复杂度 O(N)。

链表因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某个元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;并且由于每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。

 

数据结构的操作

  所有的操作归根结底无非就是:增、删、改、查四种操作,对于数组而言这四种操作都是比较简单的,对于链表可能稍微复杂,在记忆的时候不是记忆的代码而是记忆的过程。

增加节点:

 

 

删除节点:

 

 

遍历节点:

在遍历时具体操作可以分为两种:迭代和递归

while(cur != null){
   cur = cur.next;
}
public void traverse(ListNode head){
   traverse(head);
}

迭代遍历相对简单,递归在后面会进行更详细的讲解。

 

算法总览

  算法就是一些优秀的具体操作的集合。算法包含:滑动窗口、双指针、快慢指针、区间合并、循环排序、原地反转链表、树上的BFS、树上的DFS、双堆、子集、变种二分、最大前K个元素、K-路归并、 拓扑排序等等,根据不同的情况可能又分为多种方法。单独而言每种方法都是比较简单的,困难的地方在于一道题目中可能是多个算法和数据结构的组合,这就导致了题目做不出来。

 

二分查找

二分查找:常用于寻找某个特定的数,时间复杂度为O(logN)。利用最简单的数据结构:数组,可以快速实现该算法。

public int binarySearch(int[] arr,int target){
    //返回-1表示失败
    if (arr == null || arr.length == 0) return -1;

    int left = 0,right = arr.length-1;
    while (left <= right){
        int mid = left+(right-left)/2;
        if (arr[mid] == target){
            return mid;
        }else if (arr[mid] < target){
            left = mid+1;
        }else {
            right = mid-1;
        }
    }
    return -1;
}

上面的代码实现的是返回特定元素的索引,当有重复元素时,这样做并不能保证查找到的是哪一个,因此有重复元素时不适用,但是如果来判断是否有该元素是可以使用的。对上述代码有几处要注意的地方:

1:当left与right相等时也要进行循环,是因为可能会有下面的情况,直接原因是在一开始声明的时候声明的是闭区间[left,right],如果right=num.length便不会包含了。

 

 

2:求mid当遇到偶数的情况,是偏向前面的

 

 

3:因为left与right的位置是可以遍历的到的,所以当mid处不是结果时便可以放心移动前或后一个位置

4:二分查找对数据的重复性是很敏感的,当题目特别指出没有重复元素时要好好考虑下了。

注:二分查找与二分法是不同的,二分法也称为分治算法是一种抽象的算法,其思路是当一个原问题复杂难以解决时,可以拆成小问题,把一个个小问题解决了,原问题也就解决了。

在二分查找有了上面的基础理解后,二分查找还有两个应用:查找左边界、查找右边界。

查找左边界

public int leftBound(int[] nums,int target){
    if (nums == null ||nums.length == 0) return -1;
    int left = 0;
    int right = nums.length;//这里和查找位置时不同

    while (left < right){//对应于right的初始值情况,循环的终止条件也要修改
        int mid = left+(right-left)/2;
        if (nums[mid] == target){
            right = mid;//当相等时,不立即返回而是向左侧收缩
        }else if (nums[mid] < target){
            //因为初始定义的范围[left,right),所以当mid被检测到时应该分为[left,mid)和[mid+1,right)
            left = mid+1;
        }else if (nums[mid] > target){
            right = mid;
        }
    }
    return left;//因为while终止的时候两者是相等的,返回谁都一样
}

查找右边界

public int rightBound(int[] nums,int target){
    if (nums == null ||nums.length == 0) return -1;
    int left = 0,right = nums.length;

    while (left < right){//对应于right的初始值情况,循环的终止条件也要修改
        int mid = left+(right-left)/2;
        if (nums[mid] == target){
            left = mid+1;//当相等时,向右侧收
        }else if (nums[mid] < target){
            //因为初始定义的范围[left,right),所以当mid被检测到时应该分为[left,mid)和[mid+1,right)
            left = mid+1;
        }else if (nums[mid] > target){
            right = mid;
        }
    }
    //因为while终止的时候两者是相等的,返回谁都一样
    //之所以要减1是因为当相等时left=mid+1,这样可能导致更新后就不一定相等了
    //其实在左边界也是一样的,因为左边界时right是取不到的,就相当于取到了right-1处位置
    return left-1;
}

在注意到上述几点后,结合不同的题目修改代码就可以了,举例题目:

LeetCode162-寻找峰值;LeetCode153-寻找旋转排序数组中的最小值;LeetCode154-寻找旋转排序数组中的最小值||;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值