一、数据结构的底层存储
数组:
- 紧凑连续存储,可以随机访问
- 可以通过索引快速找到对应元素,相对节约存储空间
- 由于连续存储,内存空间必须一次性分配
- 若要扩容,需要重新分配一块更大的空间,再把数据全部复制过去
- 若要进行插入或删除,每次必须搬移后面的所有数据以保持连续
链表:
- 不连续存储,靠指针指向下一个元素的位置————不存在数组扩容问题
- 若知道某一元素的前驱和后继,操作指针即可删除该元素或插入新元素
- 由于存储空间不连续,不能根据一个索引算出对应元素的地址,即不能随机访问
- 由于每个元素必须存储前后元素位置的指针,会消耗更多的存储空间
二、数据结构的基本操作
基本操作:遍历或访问——> 增删改查
遍历/访问的基本形式:线性——for/while迭代和非线性——迭代
数组遍历框架:
void traverse(int[] arr){
for(int i = 0; i < arr.length;i++){
//访问arr[i]
}
}
链表遍历框架:
//基本的单链表结构
clas ListNode{
int val;
ListNode next;
}
void traverse(ListNode head){
for(ListNode p = head;p != null;p = p.next){
//访问 p.val
}
}
void traverse(ListNode head){
traverse(head.next)
}
二叉树遍历框架:
//基本的二叉树节点
class TreeNode{
int val;
TreeNode left,right;
}
void traverse(TreeNode root){
traverse(root.left);
traverse(root.right);
}
N叉数遍历框架:
class TreeNode{
int val;
TreeNode[] children;
}
void traverse(TreeNode root){
for(TreeNode child:root.children)
traverse(child)
}
三、单链表操作
1. 合并两个有序列表
我的思路:
class ListNode{
int val;
ListNode next;
}
ListNode mergeTwoLists(ListNode l1, ListNode l2){
ListNode res = null; //这里写错了
while(l1.next != null || l2.next != null){// 条件判别有问题
if(l1.val < l2.val){
res.next = l1;
l1 = l1.next;
}
else if(l1.val > l2.val){
res.next = l2;
l2 = l2.next;
}else{
res.next=l1;
l1 = l1.next;
l2 = l2.next;
}
}
if(l1 != null){
res.next = l1;
}
if(l2 != null){
res.next = l2;
}
return res.next;
}
参考代码思路:
ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 虚拟头结点
ListNode dummy = new ListNode(-1), p = dummy;
ListNode p1 = l1, p2 = l2;
while (p1 != null && p2 != null) {
// 比较 p1 和 p2 两个指针
// 将值较小的的节点接到 p 指针
if (p1.val > p2.val) {
p.next = p2;
p2 = p2.next;
} else { //剩余的条件是<=,等于是可以包含进来的
p.next = p1;
p1 = p1.next;
}
// p 指针不断前进
p = p.next; // 一开始p指向虚拟节点
}
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return dummy.next;
}
总结
:
- 不要对链表直接操作,应该有一个索引指针,用该指针来
- 可以设置虚拟节点,但不是设为null,返回时只要返回虚拟节点的next就行了
- 注意 java 是 new 一个出来
- 注意代码的简洁性
注: 当你需要创建一条新链表的时候,可以使用虚拟节点简化边界情况的处理(dummy节点)
2. 链表的分解
我的思路:
public ListNode partition(ListNode head, int x) {
ListNode left = new ListNode(-1);
ListNode right = new ListNode(-1);
ListNode p1 = left;
ListNode p2 = right;
ListNode q = head;
while(q != null){
if(q.val < x){
p1.next = q;
p1 = p1.next;
}else{
p2.next =q;
p2 = p2.next;
}
q = q.next; //错误
}
p1.next = right.next;
return left.next;
}
参考代码:
ListNode partition(ListNode head, int x) {
// 存放小于 x 的链表的虚拟头结点
ListNode dummy1 = new ListNode(-1);
// 存放大于等于 x 的链表的虚拟头结点
ListNode dummy2 = new ListNode(-1);
// p1, p2 指针负责生成结果链表
ListNode p1 = dummy1, p2 = dummy2;
// p 负责遍历原链表,类似合并两个有序链表的逻辑
// 这里是将一个链表分解成两个链表
ListNode p = head;
while (p != null) {
if (p.val >= x) {
p2.next = p;
p2 = p2.next;
} else {
p1.next = p;
p1 = p1.next;
}
// 断开原链表中的每个节点的 next 指针
ListNode temp = p.next;
p.next = null;
p = temp;
}
// 连接两个链表
p1.next = dummy2.next;
return dummy1.next;
}
总结:
- 比较完之后,有一个将原链表与已找到的元素断开的一步,否则,会让P1和P2连接在一块【画图就知道了】
3.合并 k 个有序链表
提示: 使用优先级队列(二叉堆)
代码参考:
ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) return null;
// 虚拟头结点
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
// 优先级队列,最小堆
PriorityQueue<ListNode> pq = new PriorityQueue<>(
lists.length, (a, b)->(a.val - b.val));
// 将 k 个链表的头结点加入最小堆
for (ListNode head : lists) {
if (head != null)
pq.add(head);
}
while (!pq.isEmpty()) {
// 获取最小节点,接到结果链表中
ListNode node = pq.poll();
p.next = node;
if (node.next != null) {
pq.add(node.next);
}
// p 指针不断前进
p = p.next;
}
return dummy.next;
}
3.1 二叉堆 Binary Heap
- 主要操作:sink(下沉) 和 swim(上浮)
- 主要应用:
- 一种排序方法——堆排序
- 一种有用的数据结构——优先级队列
- 主要思想:逻辑上是一种特殊的二叉树(完全二叉树),但存在数组里——可以将数组索引作为指针
// 父节点的索引
int parent(int root) {
return root / 2;
}
// 左孩子的索引
int left(int root) {
return root * 2;
}
// 右孩子的索引
int right(int root) {
return root * 2 + 1;
}
举例: 看 R 节点,其父节点在数组中的位置是 3/2=1,其左孩子节点在数组中的位置是 32=6(O),右孩子节点在数组中的位置是 32+1 = 7(A)
- 分类:
- 最大堆:每个节点都大于等于它的两个子节点
- 最小堆:每个节点都小于等于它的子节点
3.2 优先级队列
重要性质:
插入或删除元素的时候,元素会自动排序——底层就是二叉堆
- 主要应用:插入一个元素和删除最大元素(最大堆)或删除最小元素(最小堆)
public class MaxPQ
<Key extends Comparable<Key>> {
// 存储元素的数组
private Key[] pq;
// 当前 Priority Queue 中的元素个数
private int size = 0;
public MaxPQ(int cap) {
// 索引 0 不用,所以多分配一个空间
pq = (Key[]) new Comparable[cap + 1];
}
/* 返回当前队列中最大元素 */
public Key max() {
return pq[1];
}
/* 插入元素 e */
public void insert(Key e) {...}
/* 删除并返回当前队列中最大元素 */
public Key delMax() {...}
/* 上浮第 x 个元素,以维护最大堆性质 */
private void swim(int x) {...}
/* 下沉第 x 个元素,以维护最大堆性质 */
private void sink(int x) {...}
/* 交换数组的两个元素 */
private void swap(int i, int j) {
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
/* pq[i] 是否比 pq[j] 小? */
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
/* 还有 left, right, parent 三个方法 */
}
下称: 若某个节点A比它的子节点(中的一个)小,那么A就不配做父节点,应该让A下去,让更大的那个节点上来
上浮: 若某个节点B比它的父节点大,那么B就不应该做子节点 ,应该让B上去,让父节点下来
注: 操作只会在堆底和堆顶进行,堆底上浮,堆顶下沉
上浮代码: 利用while循环,让x不断上去
public class MaxPQ <Key extends Comparable<Key>> {
// 为了节约篇幅,省略上文给出的代码部分...
private void swim(int x) {
// 如果浮到堆顶,就不能再上浮了
while (x > 1 && less(parent(x), x)) {
// 如果第 x 个元素比上层大
// 将 x 换上去
swap(parent(x), x);
x = parent(x);
}
}
}
举例: x 是子节点,P和T互换,x = p,T=p.parent(x),所以再令x = T,去看STR这个枝干——交换之后,继续上浮,让x始终做上一层的子节点,去与其新的父节点比较
下沉代码:
public class MaxPQ <Key extends Comparable<Key>> {
// 为了节约篇幅,省略上文给出的代码部分...
private void sink(int x) {
// 如果沉到堆底,就沉不下去了
while (left(x) <= size) {
// 先假设左边节点较大
int max = left(x);
// 如果右边节点存在,比一下大小
if (right(x) <= size && less(max, right(x)))
max = right(x);
// 结点 x 比俩孩子都大,就不必下沉了
if (less(max, x)) break;
// 否则,不符合最大堆的结构,下沉 x 结点
swap(x, max);
x = max;
}
}
}
插入insert: 将要插入的元素添加到堆底的最后,然后让其上浮到正确位置
public class MaxPQ <Key extends Comparable<Key>> {
// 为了节约篇幅,省略上文给出的代码部分...
public void insert(Key e) {
size++;
// 先把新元素加到最后
pq[size] = e;
// 然后让它上浮到正确的位置
swim(size);
}
}
delMax: 先把堆定元素和堆底最后的元素B对调,然后删除A,让B下沉到正确位置
public class MaxPQ <Key extends Comparable<Key>> {
// 为了节约篇幅,省略上文给出的代码部分...
public Key delMax() {
// 最大堆的堆顶就是最大元素
Key max = pq[1];
// 把这个最大元素换到最后,删除之
swap(1, size);
pq[size] = null;
size--;
// 让 pq[1] 下沉到正确位置
sink(1);
return max;
}
}
注: 插入和删除元素的时间复杂度为 O(logK)