数据结构&算法
什么是线性表?
笼统的说就是把所有数据串成一串,在存储在物理空间中,稍微官方的点说具有一对一关系的数据线性的存储在物理空间中,这种存储结构就为线性表。
线性表分为?
顺序表(顺序存储结构):数据集中存放,将数据存储在连续的物理空间中,这种存储结构就为顺序存储结构(顺序表)
优点:1:内存连续,遍历速度快 2:根据下标查找速度快
缺点: 1:长度固定,需要在在使用前预估长度 2:在进行增加修改操作时候时间复杂度高
链表(链式存储结构):数据分散存放,将数据分散的存储在物理空间的各个地方,然后用一条“线”保存他们的逻辑关系 ,这种存储结构为链式存储结构(链表)这里说的“线”其实就是链表有两个区域分别为 数据域和指针域,而他们的逻辑关系就靠指针区来维持。
优点:1:在需要使用的时候不要预估长度 2:链表使用不连续存储空间这要就可可以充分的利用内存空间,实现灵活的内存动态管理
缺点:1:链表需要占用内存占用的资源要比顺序表占用的资源的多,因为在链表中是一个个Node节点,包括指针区和数据区所以说使用起来占用的资源和消耗的内存要多于顺序表。2:查找结点或者遍历节点时间复杂度高
常见的线性表分类:
1:单项链表
2:双向链表
3:循环链表
4:双向循环链表
链表与顺序表的区别?
顺序表:
- 方便遍历查找 但是增删改时间复杂读高
- 数据存储连续
- 长度固定,如果申明顺序表的长度过小,需要重新申请更大存储空间
- 一次开辟 永久使用
链表:
- 方便进行增删改操作 遍历查找时间复杂度高
- 数据存储不连续 分散
- 链表则不需要扩容,动态的进行扩容
- 链表开辟的是一个node节点,随时需要随时开辟
线性表中的手撕代码
1.单向链表的参考实现
(单线链表的尾插法)实现思路;获取链表的尾部节点,然后在获取当前想要插入的节点,判断当前节点是否为空节点,如果为空节点说明这个链表中没有节点,将链表中首节点和位节点指向都指向添加进来的节点,如果不为空说明链表中还有节点,将原来的节点的指针区的下一个next指向新的节点。
// 添加链表元素(尾插法) public void add(int val) { // 获取链表的尾节点 final Node l = last; // 创建新节点 final Node newNode = new Node(val); // 判断原来的尾节点是否等于null if (l != null) { l.next = newNode; //让原来的尾节点的next -> 新节点 } else { first = newNode; //首节点 -> 新节点 } last = newNode; //尾节点 -> 新节点 size++; // 链表长度递增 }
2.合并有序链表
思路实现:将两个有序的链表合并后依然是有序的实现思路为,首先获取两个链表首节点,创建一个新的链表用来保存新的节点值,比较两个节点的值的大小,将较小的存储在这个新链表中,然后再再将较小节点向后移动一位在进行比较,就这样反复比较就可以比较出来,但是有一种情况就是两个链表的长度万一不一样长,这时候就得另做判断,判断当前的链表的第一个首节点是否为空,谁为空说明说明谁的链表就短的么个节点,将长的链表的首节点加入新的链表中,顺便将长的链表重新指向下一个节点。整体在一大的循环里条件为链表都不为空。
public static Linked meger(Linked l1, Linked l2) { // 分别使用p1和p2,记录两个链表l1和l2的移动位置,默认为“头结点”位置 Node p1 = l1.first, p2 = l2.first; // 用于保存"合并结果"的链表 Linked result = new Linked(); while (p1 != null || p2 != null) { // p1等于null,代表链表l1中的节点合并完毕,需要合并链表l2的剩余节点 if (p1 == null) { result.add(p2.val); p2 = p2.next; continue; } // p2等于null,代表链表l2中的节点合并完毕,需要合并链表l1的剩余节点 if (p2 == null) { result.add(p1.val); p1 = p1.next; continue; } if (p1.val < p2.val) { // 如果p1小,则添加p1的值,并移动p1 result.add(p1.val); p1 = p1.next; } else { // 如果p2小,则添加p2的值,并移动p2 result.add(p2.val); p2 = p2.next; } } return result; }
3.反转链表
实现思路: 实现链表的逆序其实是借助了一个几何结构后stack,stack遵循的规则就是先进后出,具体过程是获取链表的首节点,判断首节点是否为空,为空说明链表为空之间返回链表,否为说明链表不为空,不为空则将链表中的节点一次压入栈中,然后遍历栈,然后创建一个链表,将栈中的元素加到链表中,就是实现的链表的反转。
public static Linked reverseLinked(Linked linked) { // 创建一个栈,用于保存链表 Stack<Node> stack = new Stack<Node>(); // 获取链表头结点,判断链表是否为空 Node currentNode = link.first; if(currentNode == null) { return link; } // 遍历链表,将链表中的节点依次入栈 while(currentNode != null) { stack.push(currentNode); currentNode = currentNode.next; } // 清空链表 link = new Linked(); // 遍历栈,将栈中的元素依次存入链表 while(!stack.isEmpty()) { link.add(stack.pop().val); } return link; }
4.实现两个链表的求和
实现思路:在求和之前我们必须知道一个在使用尾插法时各位是最后插进来的,头节点也是最后插进来,然后获取两个链表的头节点将两个链表中的节点值加起来(在一个循环条件为连个头节点不为null),但是有一种情况就是,节点数不一样就意味着在加的同时该位置的值为null所以在这里需要判断如果为空默认为0 ,还要考虑一种情况就是满是进位怎么办,设置一个int类型的变量用来保存进位的值然后加在两个节点的和上,将计算出来值和10取余的值放在链表中,还要考虑就是万一在高位时候遇到进位怎么办?判断进位值不等于0时,直接将进位存在链表中。
public static Linked addTwoNumbers(Linked l1, Linked l2) { // 获取两个链表的头节点(从头(低位)开始计算) Node n1 = l1.first; Node n2 = l2.first; Linked result = new Linked(); int carry = 0; while (n1 != null || n2 != null) { // 获取计算位的值 int x = n1 != null ? n1.val : 0; int y = n2 != null ? n2.val : 0; // 计算当前位置数字之和(进位) int sum = x + y + carry; // 计算进位数 carry = sum / 10; // 计算结果并存入链表 result.add(sum % 10); //如果需要进位,则仅保存余数至结果链表中 if (n1 != null) { n1 = n1.next; } if (n2 != null) { n2 = n2.next; } } // 如果存在最高位进位,则将进位数保存至结果链表中 if (carry != 0) { result.add(carry); } return result; }
5.判断链表是否有环
方法一:使用set集合
实现思路:首先将链表中节点加入集合set中,然后利用set的.contains()方法判断是否有相同的节点,如果有相同的就返回true否则false,这个方法虽然简单但是比较消耗资源,因为需要多维护一个集合。
private static boolean hasCycle(Node node) { Set<Node> nodeSet = new HashSet<>(); while (node != null) { if (nodeSet.contains(node)) { return true; } nodeSet.add(node); node = node.next; } return false; }
5.1判断链表是否有环
方法二:使用快慢指针
实现思路为:先指定两个快慢节点,快节点要在当前节点下两位,慢节点在当前节点的下一位,所以如果有有环的话一定会有两个节点的相等的时候。
private static boolean hasCycle(Node node) { if (node == null) { return false; } Node fast = node; Node slow = node; while (fast != null && fast.next != null && slow != null) { fast = fast.next.next; // 快指针移动2步 slow = slow.next; // 慢指针移动1步 if (fast == slow) { // 如果碰面,就代表链表有环 return true; } } return false; }
6判断链表是否相交
方法一双重循环判断是否有相同的值:
实现思路:遍历两个链表然后判断是否有相同在值,有ture说明有环false说明没有。
public static boolean isIntersect1(Linked link1, Linked link2) { for (Node p = link1.first; p != null; p = p.next) { for (Node q = link2.first; q != null; q = q.next) { if (p == q) { return true; } } } return false; }
方法二 长短指针的方法
实现思路:在链表类力维护一个计算链表数量的方法,长度不一样时候通过长度获得长指针的节点和短指针节点,相减让长指针先向下一个走几步,直到和断指针一样,然后判断这两节点是否一样,不一样就下一个,一样话判断是否位null不是就返回true说明有相交,否则返回false,为什么还要判断null因为如果真的没有相交最后都为null说明也是没有相交的。
public static boolean isIntersect2(Linked link1, Linked link2) { // 安全检测 if (link1 == null || link2 == null) { return false; } // p 指向长链表的第一个结点 // q 指向短链表的第一个结点 Node p = link1.size() > link2.size() ? link1.first : link2.first; Node q = link1.size() > link2.size() ? link2.first : link1.first; // 求两个链表长度差 int diff = Math.abs(link1.size() - link2.size()); // p先往后移动diff个结点 while (diff-- > 0) { p = p.next; } // p 和 q 同时往后移动 while (p != q) { p = p.next; q = q.next; } // 如果p(q)不为null,则两个链表相交,否则不相交() if (p != null) { return true; } else { return false; } }