赞
踩
数据结构和算法作为程序员的基本功,一定得稳扎稳打的学习,我们常见的框架底层就是各类数据结构,例如跳表之于redis、B+树之于mysql、倒排索引之于ES,熟悉了底层数据结构,对框架有了更深层次的理解,在后续程序设计过程中就更能得心应手。掌握常见数据结构和算法的重要性显而易见,本文主要讲解了几种常见的数据结构及基础的排序和查找算法,最后对高频算法笔试面试题做了总结。本文会持续补充,希望对大家日常学习或找工作有所帮忙。本文是第二讲:数组和链表
定义:是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。
class Solution { public List<List<Integer>> threeSum(int[] nums) { Arrays.sort(nums);//由小到大 List<List<Integer>> ls = new ArrayList<>(); for (int i = 0; i < nums.length - 2; i++) { if (i == 0 || (i > 0 && nums[i] != nums[i - 1])) { // 跳过可能重复的答案 int l = i + 1, r = nums.length - 1, sum = 0 - nums[i]; while (l < r) { if (nums[l] + nums[r] == sum) { ls.add(Arrays.asList(nums[i], nums[l], nums[r])); while (l < r && nums[l] == nums[l + 1]) l++; while (l < r && nums[r] == nums[r - 1]) r--; l++; r--; } else if (nums[l] + nums[r] < sum) { while (l < r && nums[l] == nums[l + 1]) l++; // 跳过重复值 l++; } else { while (l < r && nums[r] == nums[r - 1]) r--; r--; } } } } return ls; } }//时间复杂度是O(n^2)
public int majorityElement(int[] nums){
int count = 1;
int maj = nums[0];
for (int i = 1; i < nums.length; i++){
if (maj == nums[i])
count++;
else {
count--;
if (count == 0) {//说明maj所代表的数不能超过一半
maj = nums[i + 1];
}
}
}//时间复杂度O(n)
return maj;
}
public int majorityElement(int[] nums){
Arrays.sort(nums);//时间复杂度O(nlgn)
return nums[nums.length / 2];
}
class Solution { public int firstMissingPositive(int[] nums) { //先排序,然后分两种情况 :有1 和 没有1 (负数略过) //1.没有1,则输出1 //2.有1 则判断下一个数和前一个数是否相等、差1或者差好几个数,相等继续,差1继续,否则退出 boolean flag = false; int i; Arrays.sort(nums); for(i=0;i<nums.length;i++) { if(nums[i]<0) continue;//负数略过 if(nums[i]==1) flag=true; if(i+1<nums.length && nums[i]==nums[i+1]) continue; if(i+1==nums.length || nums[i]+1!=nums[i+1]) break; } if(flag==true) return nums[i]+1; if(flag==false) return 1; return 0; } }//时间复杂度O(n)
Demo 五子棋程序中,有存盘退出和续上盘的功能,因为该二维数组中的很多值默认为0,因此记录了很多没有意义的数据 --》稀疏
git中的分支管理
当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git 仓库中这些校验和保存为树对象。随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git 就可以在需要的时候重现此次保存的快照。
现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个 树 对象 (记录着目录结构和 blob 对象索引)以及一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)
做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 master 分支会在每次提交时自动向前移动
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
循环链表:循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表(比如著名的约瑟夫问题)
ListNode p = null;//在单链表的基础之上,链尾指向链头
q =p;
for (int i = 2; i <= N; i++) {
p = p.getNext();
p.setVal(i);
}
p.setNext(q);//构建循环链表
在遍历循环链表时得特别小心,否则将会无限地遍历链表,因为循环链表每一个结点都有一个后继结点
双向链表:(需要额外的两个空间来存储后继结点next和前驱结点的地址prev)
public class ListNode {
int value;
ListNode prev;
ListNode next;
ListNode(int key, int val) {
this.key = key;
this.value = val;
}
}
使用技巧:
1、理解指针或引用的含义:是存储所指对象的内存地址(将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针)
2、警惕指针丢失和内存泄漏 java不需考虑(使用jvm自动管理内存)
3、利用哨兵简化实现难度:如果我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点(插入排序、归并排序、动态规划)
删除最后一个结点和删除其他节点,插入第一个结点和插入其他节点可以统一为相同的代码逻辑。
哨兵的好处:它可以减少特殊情况的判断,比如判空,判越界,因为空可越界可认为是小概率情况,如实每次执行代码都走一遍,大多数情况下是多于的。
比如给一个哨兵节点,以及将key赋值给末尾元素,让数组遍历不用判断越界也可以因为相等停下来。
4、重点留意便捷条件处理:(如果链表为空时,代码是否能正常工作?如果链表只包含一个结点时,代码是否能正常工作?代码逻辑在处理头结点和尾结点的时候,是否能正常工作?)
5、举例画图,辅助思考:(举例法和画图法)
开发中使用集合工具包:Collections.reverse(List<?> list)
原理:i m n相邻,调整指针的指向,调整m的指向,指向结点i,链表会断开,需要在调整之前把n保存起来 代码P236
public class 链表反转 { //单链表的反转 调整指针的指向,在调整next指针之前,需要保存前一个值 反转后链表的头结点为原始链表的尾节点,即next为空指针的节点 public void reverseIteratively(Node head) { Node pReversedHead = head; Node pNode = head; Node pPrev = null; while (pNode != null) { Node pNext = pNode.next; if (pNext == null) { pReversedHead = pNode;//pNode此时为最后一个结点 反转后链表的头结点为原始链表的尾节点 } pNode.next = pPrev; pPrev = pNode; pNode = pNext; } head = pReversedHead; }
*思路2:使用散列表(时间复杂度O(n),空间复杂度O(n))
从表头节点开始,逐一遍历链表中的每个结点;
对于每个结点,检查该结点的地址是否存在于散列表中;
如果存在,则表明当前访问的结点已经被访问过,出现此情况的原因是给定的链表中存在环;
如果散列表中没有当前节点的地址,那么把该地址插入散列表中;
重复上述过程,直至到达表尾或找到环。
public static boolean checkCircle(Node list){
if (list == null) {
return false;
}
Node fast = list.next;
Node slow = list;
while (fast != null && fast.next !=null) {
fast = fast.next.next;
slow = slow.next;
if (slow ==fast) {
return true;
}
}
return false;
}//时间复杂度O(n) 空间复杂度O(1)
对floyd算法的补充:如果两个指针每次分别移动2个结点和3个结点,而不是移动一个和2个结点,算法仍然有效吗?
可以,算法的复杂度可能增加
思路:在找到链表中的环后,保持slowPtr指针不变,fastPtr指针则继续移动,每次移动fastPtr指针时,计数器变量加1,直至再一次回到slowPtr指针所在的位置,即为环的长度。
public class 检测环的长度 { int FindLoopLength(ListNode head){ ListNode slowPtr =head,fastPtr =head; boolean loopExists = false; int counter = 0; if (head == null) { return 0; } while (fastPtr.next != null && fastPtr.next.next != null) { slowPtr = slowPtr.next; fastPtr = fastPtr.next.next; if (slowPtr == fastPtr) { loopExists =true; break; } } if (loopExists) { fastPtr =fastPtr.next; while (slowPtr != fastPtr) { fastPtr =fastPtr.next; counter++; } return counter; } return 0; //链表中不存在环 } } //时间复杂度O(n)
补充:此思路可以引申为 求循环小数的开始位置(小数点之后的位数)和循环长度
public static Node deleteLastKth(Node list,int k){ Node fast =list; int i =1; while (fast !=null && i<k) { fast =fast.next; ++i;//第一个指针先走k步 } if (fast ==null) { return list; } Node slow =list; Node prev =null; while (fast.next !=null) { fast = fast.next; prev =slow; //prev为倒数第k个数 slow =slow.next; } if (prev ==null) { list = list.next; }else { prev.next =prev.next.next; } return list; }//时间复杂度O(n)
思路2:蛮力法(时间复杂度最高)
从链表的第一个结点开始,统计当前节点后面的结点个数。如果后面的节点个数小于k-1,算法结束;如果大于k-1,则移动到下一个结点,重复该过程
思路3:散列表O(m) 为了减少链表遍历的次数
散列表的条目是<结点的位置,结点地址>,在遍历链表时,可以得到链表的长度,令M表示链表的长度,这样求链表的导师胡第n个结点的问题转变为求链表正数
第M-n+1个结点。返回散列表中主键为M-n+1的值即可。时间复杂度O(m),空间复杂度O(m):创建一个大小为M的散列表。
public class 找到链表的中间节结点 { ListNode FindMiddle(ListNode head) { ListNode ptr1x, ptr2x; ptr1x = ptr2x = head; int i = 0; //不断循环,直至第一个指针到达表尾 while (ptr1x.getNext() !=null) { if (i == 0) { ptr1x =ptr1x.getNext();//只移动第一个指针 i = 1; } else if (i== 1) { ptr1x = ptr1x.getNext(); ptr2x = ptr2x.getNext(); i =0; } } return ptr2x;//返回ptr2x的值,即为中间结点 } }//时间复杂度O(n) 空间复杂度O(1)
思路:使用分治的思想,两两归并
class Solution { public ListNode mergeKLists(ListNode[] lists) { if (lists.length == 0) return null; if (lists.length == 1) return lists[0]; if (lists.length == 2) { return mergeTwoLists(lists[0], lists[1]); } int mid = lists.length/2; ListNode[] l1 = new ListNode[mid]; for(int i = 0; i < mid; i++){ l1[i] = lists[i]; } ListNode[] l2 = new ListNode[lists.length-mid]; for(int i = mid,j=0; i < lists.length; i++,j++){ l2[j] = lists[i]; } return mergeTwoLists(mergeKLists(l1),mergeKLists(l2)); } //两个有序链表合并为一个新的有序链表 递归的方法 public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) return l2; if (l2 == null) return l1; ListNode head = null; if (l1.val <= l2.val){ head = l1; head.next = mergeTwoLists(l1.next, l2); } else { head = l2; head.next = mergeTwoLists(l1, l2.next); } return head; } }
public class 在有序链表中插入一个结点 { ListNode InsertSortedList(ListNode head, ListNode newNode){ ListNode current =head; ListNode temp = null; if (head ==null) { return newNode; } //遍历链表,直至找到比新节点中数据值更大的节点 while (current != null && current.val < newNode.val) { temp = current;//temp为current的上一个节点 current = current.next; //current为比newNode值大的数 } //在该结点前插入新节点 newNode.setNext(current); temp.setNext(newNode); return null; } }//时间复杂度O(n)
方法1:蛮力法
把第一个链表中的每一个结点指针与第二个链表中的每一个结点指针比较,当结点相等时,即为相交结点。时间复杂度为O(mn)
方法2:散列表
选择结点较少的链表(若链表长度未知,那么随便选择一个链表),将其所有结点的指针值保存在散列表中;遍历另一个链表,对于该链表中的每一个结点,检查散列表
中是否已经保存了其结点指针。如果两个链表存在合并点,那么必定会在散列表中找到记录。时间复杂度O(m)+O(n);空间复杂度O(m)或O(n)
方法3:两个栈
创建两个栈,然后遍历两个链表,分别把所有结点存入第一个和第二个栈,两个栈包含了对应链表的结点地址,比较两个栈的栈顶元素,如果相等,则弹出两个栈
的栈顶元素并保存在临时变量中,继续上述操作,直至两个栈的栈顶元素不相等,此时即找到了两个链表的合并点。时间复杂度O(m+n),空间复杂度O(m+n)
方法4:时间复杂度超低的解法
获取两个链表L1/L2的长度,O(max(m,n));计算两个长度的差d,从较长链表的表头开始,移动d步,然后两个链表同时移动,直至出现两个后继指针相等的情况。
public class 求两个链表的合并点 { ListNode FindIntersectingNode(ListNode list1, ListNode list2){ int L1=0,L2=0,diff=0;//L1为第一个链表的长度,L2为第二个链表的长度,diff为两链表的差值 ListNode head1=list1,head2=list2; while (head1 !=null) { L1++; head1 = head1.getNext(); } while (head2 !=null) { L2++; head2 = head2.getNext(); } if (L1<L2) { head1 = list2; head2 = list1; diff = L2-L1; } else { head1 = list1; head2 = list2; diff = L1-L2; } for (int i = 0; i < diff; i++) { head1 = head1.getNext(); } while (head1 != null && head2 != null) { if (head1 == head2) { return head1; } head1= head1.getNext(); head2 = head2.getNext(); } return null; } }//时间复杂度O(max(m,n)) 空间复杂度O(1)
1)前提:字符串以单个字符的形式存储在单链表中。
2)遍历链表,判断字符个数是否为奇数,若为偶数,则不是。
3)将链表中的字符倒序存储一份在另一个链表中。
4)同步遍历2个链表,比较对应的字符是否相等,若相等,则是水仙花字串,否则,不是。
思路2:使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。在慢指针前进的过程中,同时修改其 next 指针,使得链表前半部分反序。最后比较中点两侧的链表是否相等
时间复杂度O(n) 空间复杂度O(1)
把后一个元素赋值给待删除节点,这样也就相当于是删除了当前元素
1. 如果待删除节点不是最后一个节点,就用他的next节点的value覆盖它的value,然后删掉它的next节点
2、如果是最后一个节点,顺序遍历o(n)
//递归版本
ListNode ReversePairRecursive(ListNode head){
ListNode temp;
if (head ==null || head.next == null) {
return head; //当前链表为空或只有一个元素
}else {
//逆置第一对
temp = head.next;
head.next = temp.next;//第一个结点的下一个为第三个结点
temp.next = head;//第一个结点变为第二个
head =temp;//第二个结点变第一个
head.next.next=ReversePairRecursive(head.next.next);
return head;
}
}
/** * @param N 人数 * @param M 需要排除的人序号 * @return 最后留下来的人 */ ListNode GetJosephusPosition(int N, int M){ ListNode p = null,q; //建立一个包含所有人的循环链表 p.setVal(1); q =p; for (int i = 2; i <= N; i++) { p = p.getNext(); p.setVal(i); } p.setNext(q);//构建循环链表 for (int count = N; count >1; --count) { for (int i = 0; i < M-1; i++) { p = p.getNext(); } p.setNext(p.getNext().getNext());//删除选手 } return p;//最后留下的勇者 }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。