Compare commits
24 Commits
4d81aef849
...
save_load_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ede89cef53 | ||
|
|
0fd548edcd | ||
| 553a7724b3 | |||
| 0852e12118 | |||
| 7dc4714ae9 | |||
| 9dc81b1489 | |||
| 7862647b30 | |||
| 6b9c8e9e0c | |||
| 0cf1896b5b | |||
| 33844786a1 | |||
| ce88ae645e | |||
| a45e5f6f37 | |||
| 6c38e9067d | |||
| ad2de51a55 | |||
| 293cc24ac5 | |||
| 9759c93a25 | |||
| ecf19f49ee | |||
| f5d5077278 | |||
| 4a390adb2a | |||
| 960d1f1dd9 | |||
| c73ea44747 | |||
| 33bf569fb0 | |||
| b921bda4bc | |||
| 9921848d9b |
@@ -48,4 +48,5 @@ class Config:
|
||||
}
|
||||
|
||||
ASE_ENGINE_URL = GLOBAL_CONFIG['ASE_ENGINE']['url']
|
||||
ASE_ENGINE_URL_TOKEN = GLOBAL_CONFIG['ASE_ENGINE']['url_token']
|
||||
ASE_ENGINE_NAMESPACE = GLOBAL_CONFIG['ASE_ENGINE']['namespace']
|
||||
@@ -8,12 +8,16 @@ current_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(current_directory)
|
||||
from flask_socketio import SocketIO, join_room, emit, Namespace
|
||||
import os
|
||||
from ...function_tools.function_manager import FunctionManager
|
||||
from apps.models.function import FunctionResponse
|
||||
from apps.function_tools.function_manager import FunctionManager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional
|
||||
import subprocess
|
||||
import threading
|
||||
from ...utils import *
|
||||
from collections import deque
|
||||
from eventlet.semaphore import Semaphore
|
||||
from apps.utils import *
|
||||
|
||||
EMPTY_CHAPTER_CHAIN = [ Chapter(1, CHAPTER_FOCUS, "本章是未打开某个具体章节时的默认章节。", "处于本章节时不会有任何章节跳转。请作为一名经验丰富的算法教师,回答用户的问题。") ]
|
||||
|
||||
@dataclass
|
||||
@@ -45,7 +49,9 @@ class ChatManager:
|
||||
self.function_manager = FunctionManager()
|
||||
self.bb = None
|
||||
self.chat_historys = None
|
||||
pass
|
||||
self.vscode_ws = None
|
||||
self._vscode_queue = deque()
|
||||
self._lock = Semaphore(1)
|
||||
|
||||
|
||||
def init(self,user_uuid,ase_client,raw_markdown,raw_markdown_prompts,raw_score_prompts,material_id,chapter_name, lesson_name, max_iter = 5, app=None, socketio=None):
|
||||
@@ -63,8 +69,41 @@ class ChatManager:
|
||||
self.bb = None
|
||||
self.chat_historys = []
|
||||
|
||||
|
||||
def send_to_vscode(self, event: str, data: dict, room: str = None, namespace: str = '/vscode'):
|
||||
"""统一发送入口:未就绪则入队,就绪则直接 emit。"""
|
||||
if room is None:
|
||||
room = self.user_uuid
|
||||
|
||||
with self._lock:
|
||||
if self.vscode_ws is not None:
|
||||
# 已就绪,直接发
|
||||
self.vscode_ws.emit(event, data, room=room, namespace=namespace)
|
||||
else:
|
||||
# 未就绪,入队缓存
|
||||
self._vscode_queue.append((event, data, room, namespace))
|
||||
|
||||
def set_vscode_ws(self, ws):
|
||||
"""在 on_login 时调用,注册 websocket 并立刻 flush 队列。"""
|
||||
with self._lock:
|
||||
self.vscode_ws = ws
|
||||
self._flush_vscode_queue_locked()
|
||||
|
||||
def _flush_vscode_queue_locked(self):
|
||||
"""发送缓存的消息(需在持锁状态下调用)。"""
|
||||
while self._vscode_queue:
|
||||
event, data, room, namespace = self._vscode_queue.popleft()
|
||||
# 这里假设 ws 仍然有效;如果发送失败可以考虑 try/except 并回滚入队
|
||||
self.vscode_ws.emit(event, data, room=room, namespace=namespace)
|
||||
def mark_vscode_disconnected(self):
|
||||
"""可在 on_disconnect 里调用,标记断开(之后会继续缓存)。"""
|
||||
with self._lock:
|
||||
self.vscode_ws = None
|
||||
self._vscode_queue.clear()
|
||||
|
||||
def disconnect(self, uuid):
|
||||
try:
|
||||
self.mark_vscode_disconnected()
|
||||
self.stop(uuid)
|
||||
self.ase_client.disconnect()
|
||||
except Exception as e:
|
||||
@@ -75,7 +114,7 @@ class ChatManager:
|
||||
nowchapter_content = self.chapter_chain[self.chapter_chain_now].chapter
|
||||
code_texts = extract_code_blocks(nowchapter_content)
|
||||
for code_text in code_texts:
|
||||
self.vscode_ws.emit('code-file', {'type':'create', 'data':code_text, 'chapter_title': self.chapter_chain[self.chapter_chain_now].title}, room=self.user_uuid ,namespace='/vscode')
|
||||
self.send_to_vscode('code-file', {'type':'create', 'data':code_text, 'chapter_title': self.chapter_chain[self.chapter_chain_now].title}, room=self.user_uuid ,namespace='/vscode')
|
||||
|
||||
|
||||
def next_chapter(self):
|
||||
|
||||
@@ -57,9 +57,14 @@ def realtime_response(config: dict, realtime_action: dict) -> None:
|
||||
# emit('paste_detected', paste_data)
|
||||
|
||||
#判断一下,如果粘帖内容过多就发送,后期可加入更多条件判断
|
||||
if bb.pasted_content and len(bb.pasted_content) > 250:
|
||||
chatmanager.ase_client.send("pasted_detected_in", {"file_path": file_path, "content": bb.pasted_content})
|
||||
|
||||
if bb.pasted_content and len(bb.pasted_content) > 200:
|
||||
print(f"粘贴内容 : {bb.pasted_content[:100]}...")
|
||||
print(f"ready to send")
|
||||
try:
|
||||
result = chatmanager.ase_client.send_text("pasted_detected_in", bb.pasted_content)
|
||||
print(f"Send result: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error sending: {e}")
|
||||
|
||||
elif rtype == "fileEdit":
|
||||
file_path = realtime_action.get("filePath")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# myapp/services/course_service.py
|
||||
import re
|
||||
from typing import Dict, List
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
from flask import current_app
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
@@ -95,39 +97,52 @@ def add_material_chapter(material_id, chapter_name, teacher_id):
|
||||
def rename_material_structure(material_id, is_chapter, chapter_name, lesson_name, new_name):
|
||||
mongo = current_app.extensions["mongo"]
|
||||
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
|
||||
# 先检查是否存在
|
||||
if not material:
|
||||
return False # 材料不存在
|
||||
|
||||
# 检查名称是否存在于列表中的辅助函数
|
||||
def name_exists_in_list(item_list, name_key, target_name):
|
||||
return any(item[name_key] == target_name for item in item_list)
|
||||
|
||||
# 查找包含指定章节名的章节
|
||||
def find_chapter_by_name(chapters, target_name):
|
||||
for chapter in chapters:
|
||||
if chapter['chapter_name'] == target_name:
|
||||
return chapter
|
||||
return None
|
||||
|
||||
# 检查章节是否存在
|
||||
target_chapter = find_chapter_by_name(material['chapters'], chapter_name)
|
||||
if not target_chapter:
|
||||
return False
|
||||
|
||||
if is_chapter:
|
||||
for chapter in material['chapters']:
|
||||
if chapter['chapter_name'] == chapter_name:
|
||||
break
|
||||
# 检查新章节名是否已存在
|
||||
if name_exists_in_list(material['chapters'], 'chapter_name', new_name):
|
||||
return False
|
||||
for chapter in material['chapters']:
|
||||
if chapter['chapter_name'] == new_name:
|
||||
return False
|
||||
else:
|
||||
for chapter in material['chapters']:
|
||||
if chapter['chapter_name'] == chapter_name:
|
||||
break
|
||||
return False
|
||||
for lesson in chapter['lessons']:
|
||||
if lesson['lesson_name'] == lesson_name:
|
||||
break
|
||||
for lesson in chapter['lessons']:
|
||||
if lesson['lesson_name'] == new_name:
|
||||
return False
|
||||
|
||||
if is_chapter:
|
||||
|
||||
# 更新章节名
|
||||
mongo.db.materials.update_one(
|
||||
{'_id': ObjectId(material_id)},
|
||||
{'$set': {'chapters.$[c].chapter_name': new_name}},
|
||||
array_filters=[{'c.chapter_name': chapter_name}]
|
||||
)
|
||||
else:
|
||||
# 检查课时是否存在
|
||||
if not name_exists_in_list(target_chapter['lessons'], 'lesson_name', lesson_name):
|
||||
return False
|
||||
|
||||
# 检查新课时名是否已存在
|
||||
if name_exists_in_list(target_chapter['lessons'], 'lesson_name', new_name):
|
||||
return False
|
||||
|
||||
# 更新课时名
|
||||
mongo.db.materials.update_one(
|
||||
{'_id': ObjectId(material_id)},
|
||||
{'$set': {'chapters.$[c].lessons.$[l].lesson_name': new_name}},
|
||||
array_filters=[{'c.chapter_name': chapter_name}, {'l.lesson_name': lesson_name}]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
|
||||
@@ -178,33 +193,65 @@ def reorder_material_structure(material_id, order):
|
||||
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
|
||||
if not material:
|
||||
return False
|
||||
|
||||
# 1. 构建原章节映射(保留章节元信息)和原章节的课时映射
|
||||
chapter_map = {ch["chapter_name"]: ch for ch in material["chapters"]}
|
||||
# 每个章节的课时映射:{章节名: {课时名: 课时对象}}
|
||||
original_chapter_lessons = {
|
||||
ch["chapter_name"]: {ls["lesson_name"]: ls for ls in ch["lessons"]}
|
||||
for ch in material["chapters"]
|
||||
}
|
||||
|
||||
# 2. 收集所有章节“少掉的课时”到全局池(仅包含原章节有但新排序没包含的课时)
|
||||
# 用列表存储,避免同名课时被覆盖(处理可能的重名场景)
|
||||
global_extra_lessons = []
|
||||
for ch_name in chapter_map:
|
||||
original_lessons = original_chapter_lessons[ch_name] # 原章节的所有课时
|
||||
# 找到当前章节在order中的配置(如果没有,默认该章节所有课时都是“少掉的”)
|
||||
chapter_order = next((item for item in order if item["chapter_name"] == ch_name), None)
|
||||
if not chapter_order:
|
||||
# 章节未在order中出现,所有原课时都是“少掉的”
|
||||
global_extra_lessons.extend(original_lessons.values())
|
||||
continue
|
||||
# 章节在order中出现,找出原课时中不在新排序里的(即“少掉的”)
|
||||
new_lesson_names = chapter_order["lessons"]
|
||||
for ls_name, ls in original_lessons.items():
|
||||
if ls_name not in new_lesson_names:
|
||||
global_extra_lessons.append(ls)
|
||||
print("global_extra_lessons:", global_extra_lessons)
|
||||
new_chapters = []
|
||||
for chapter_order in order:
|
||||
ch_name = chapter_order['chapter_name']
|
||||
ch_name = chapter_order["chapter_name"]
|
||||
if ch_name not in chapter_map:
|
||||
continue # 如果前端传了不存在的章节名,可以选择跳过或报错
|
||||
continue # 跳过不存在的章节
|
||||
|
||||
original_ch = chapter_map[ch_name]
|
||||
|
||||
# 建立 lesson_name -> lesson dict
|
||||
lesson_map = {ls["lesson_name"]: ls for ls in original_ch["lessons"]}
|
||||
|
||||
# 按顺序重排 lessons
|
||||
original_lessons = original_chapter_lessons[ch_name] # 本章节原课时
|
||||
new_lessons = []
|
||||
for ls_name in chapter_order['lessons']:
|
||||
if ls_name in lesson_map:
|
||||
new_lessons.append(lesson_map[ls_name])
|
||||
|
||||
# 如果还有遗漏(前端没传的),可以追加到最后,避免丢数据
|
||||
for ls_name, ls in lesson_map.items():
|
||||
if ls not in new_lessons:
|
||||
new_lessons.append(ls)
|
||||
# 3. 构建当前章节的新课时列表
|
||||
for ls_name in chapter_order["lessons"]:
|
||||
# 优先使用本章节原有的课时
|
||||
if ls_name in original_lessons:
|
||||
new_lessons.append(original_lessons[ls_name])
|
||||
continue
|
||||
# 本章节没有,从全局池(其他章节少掉的课时)中找
|
||||
# 处理可能的重名:找到第一个匹配的课时
|
||||
for i, extra_ls in enumerate(global_extra_lessons):
|
||||
if extra_ls["lesson_name"] == ls_name:
|
||||
new_lessons.append(extra_ls)
|
||||
del global_extra_lessons[i] # 从全局池移除,避免重复
|
||||
break
|
||||
|
||||
# 构造新的 chapter
|
||||
# 4. 构造新章节(保留原章节元信息,替换课时列表)
|
||||
new_chapter = {**original_ch, "lessons": new_lessons}
|
||||
new_chapters.append(new_chapter)
|
||||
|
||||
# 更新 material
|
||||
# 5. 处理全局池中剩余的课时(未被任何章节引用的,追加到最后一个章节)
|
||||
if global_extra_lessons and new_chapters:
|
||||
new_chapters[-1]["lessons"].extend(global_extra_lessons)
|
||||
print("new_chapters:", new_chapters)
|
||||
# 更新数据库
|
||||
mongo.db.materials.update_one(
|
||||
{"_id": ObjectId(material_id)},
|
||||
{
|
||||
@@ -216,6 +263,7 @@ def reorder_material_structure(material_id, order):
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def save_materials_markdown_cos(
|
||||
material_id: str,
|
||||
chapter_name: str,
|
||||
@@ -404,10 +452,10 @@ def _default_markdowns(lesson_name: str):
|
||||
|
||||
## 阶段一:示例阶段
|
||||
### 步骤A
|
||||
请基于“步骤A”的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
|
||||
请基于"步骤A"的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
|
||||
|
||||
### 步骤B
|
||||
请基于“步骤B”的教学目标与材料,生成指导性提示词。
|
||||
请基于"步骤B"的教学目标与材料,生成指导性提示词。
|
||||
"""
|
||||
|
||||
score_prompt_md = f"""# {lesson_name}
|
||||
@@ -417,8 +465,134 @@ def _default_markdowns(lesson_name: str):
|
||||
|
||||
## 阶段一:示例阶段
|
||||
### 步骤A
|
||||
对学员在“步骤A”的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
|
||||
对学员在"步骤A"的输出进行评分与文字反馈,分数区间 0-100,并输出 JSON:```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
|
||||
### 步骤B
|
||||
同上,对“步骤B”输出进行评分与反馈。
|
||||
同上,对"步骤B"输出进行评分与反馈。
|
||||
"""
|
||||
return lesson_md, prompt_md, score_prompt_md
|
||||
|
||||
def save_learning_progress(user_uuid: str, chatmanager, user_id: str) -> bool:
|
||||
"""
|
||||
保存用户学习进度到MongoDB
|
||||
|
||||
参数:
|
||||
- user_uuid: 用户UUID
|
||||
- chatmanager: ChatManager实例,包含学习进度数据
|
||||
- user_id: 用户ID
|
||||
|
||||
返回:
|
||||
- bool: 保存是否成功
|
||||
"""
|
||||
try:
|
||||
# 获取MongoDB连接
|
||||
mongo = current_app.extensions["mongo"]
|
||||
|
||||
# 获取MultiAgents框架中的对话列表
|
||||
dialog_list = get_multiagents_dialog_list(user_id)
|
||||
|
||||
# 构建学习进度数据
|
||||
progress_data = {
|
||||
"user_uuid": user_uuid,
|
||||
"user_id": user_id,
|
||||
"material_id": chatmanager.material_id,
|
||||
"chapter_name": chatmanager.chapter_name,
|
||||
"lesson_name": chatmanager.lesson_name,
|
||||
"chapter_chain_now": chatmanager.chapter_chain_now,
|
||||
"chat_historys": chatmanager.chat_historys,
|
||||
"scores": chatmanager.scores,
|
||||
"multiagents_dialogs": dialog_list,
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
|
||||
# 保存到MongoDB,使用upsert操作(如果存在则更新,不存在则插入)
|
||||
mongo.db.learning_progress.update_one(
|
||||
{
|
||||
"user_uuid": user_uuid,
|
||||
"material_id": chatmanager.material_id,
|
||||
"chapter_name": chatmanager.chapter_name,
|
||||
"lesson_name": chatmanager.lesson_name
|
||||
},
|
||||
{"$set": progress_data},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
current_app.logger.info(f"学习进度保存成功: user_uuid={user_uuid}, lesson={chatmanager.lesson_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"保存学习进度失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
|
||||
"""
|
||||
从MultiAgents框架获取用户对话列表
|
||||
|
||||
参数:
|
||||
- user_id: 用户ID
|
||||
|
||||
返回:
|
||||
- List[Dict]: 对话列表
|
||||
"""
|
||||
try:
|
||||
# 从配置中获取ASE引擎URL和token
|
||||
config = current_app.config
|
||||
url_token = config.get("ASE_ENGINE_URL_TOKEN", "")
|
||||
|
||||
# 构建请求URL
|
||||
base_url = config.get("ASE_ENGINE_URL", "")
|
||||
api_url = f"{base_url}/api/sync/download"
|
||||
|
||||
# 构建请求参数
|
||||
payload = {
|
||||
"namespace_url": url_token, # 使用配置中的token作为namespace_url
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
# 发送POST请求
|
||||
response = requests.post(
|
||||
api_url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取MultiAgents对话列表异常: {str(e)}")
|
||||
return []
|
||||
|
||||
def load_learning_progress(user_uuid: str, material_id: str, chapter_name: str, lesson_name: str) -> Optional[Dict]:
|
||||
"""
|
||||
加载用户学习进度
|
||||
|
||||
参数:
|
||||
- user_uuid: 用户UUID
|
||||
- material_id: 课程ID
|
||||
- chapter_name: 章节名
|
||||
- lesson_name: 课时名
|
||||
|
||||
返回:
|
||||
- Optional[Dict]: 学习进度数据,如果不存在则返回None
|
||||
"""
|
||||
try:
|
||||
mongo = current_app.extensions["mongo"]
|
||||
progress = mongo.db.learning_progress.find_one({
|
||||
"user_uuid": user_uuid,
|
||||
"material_id": material_id,
|
||||
"chapter_name": chapter_name,
|
||||
"lesson_name": lesson_name
|
||||
})
|
||||
|
||||
if progress:
|
||||
# 转换ObjectId为字符串
|
||||
if "_id" in progress:
|
||||
progress["_id"] = str(progress["_id"])
|
||||
return progress
|
||||
return None
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"加载学习进度失败: {str(e)}")
|
||||
return None
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
# myapp/sockets/namespaces.py
|
||||
import json
|
||||
import time
|
||||
from flask import current_app, request, session
|
||||
from flask_socketio import Namespace, join_room, leave_room, emit
|
||||
|
||||
from ..function_tools import judge
|
||||
from ..function_tools import next_chapter
|
||||
from ..services.backboard_service import realtime_response
|
||||
from ..services.markdown_service import load_full_markdown_file
|
||||
from ..services.markdown_service import load_full_markdown_file
|
||||
from ..services.course_service import save_learning_progress
|
||||
from ..extension_ase.ase_client import HSAEngineClient
|
||||
from ..services.user_service import get_or_load_current_user
|
||||
|
||||
|
||||
|
||||
class VSCodeNamespace(Namespace):
|
||||
|
||||
|
||||
|
||||
def on_login(self,data):
|
||||
ex = current_app.extensions
|
||||
print("VSCode client connected")
|
||||
@@ -24,8 +29,7 @@ class VSCodeNamespace(Namespace):
|
||||
print(f"User {user_uuid} connected with path: {path}")
|
||||
join_room(user_uuid, namespace='/vscode')
|
||||
chatmanager = ex['user_uuid2chatmanager'][user_uuid]
|
||||
chatmanager.vscode_ws=self
|
||||
|
||||
chatmanager.set_vscode_ws(self)
|
||||
|
||||
|
||||
def on_load_chapter(self, data):
|
||||
@@ -68,8 +72,32 @@ class AgentNamespace(Namespace):
|
||||
chatmanager = user_uuid2chatmanager[user_uuid]
|
||||
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
|
||||
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
|
||||
chatmanager.chat_historys = []
|
||||
chatmanager.scores = []
|
||||
|
||||
# 尝试加载用户学习进度
|
||||
from ..services.course_service import load_learning_progress
|
||||
progress = load_learning_progress(user_uuid, course_id, chapter_name, lesson_name)
|
||||
need_skip_chapters = 0
|
||||
|
||||
if progress:
|
||||
# 恢复学习进度数据
|
||||
if 'scores' in progress:
|
||||
# 根据scores的数量确定需要跳过的章节数
|
||||
need_skip_chapters = len(progress['scores'])
|
||||
chatmanager.scores = progress['scores']
|
||||
print(f"恢复用户学习进度: {user_uuid}, 已完成 {need_skip_chapters} 个章节")
|
||||
|
||||
# 恢复聊天历史
|
||||
if 'chat_historys' in progress:
|
||||
chatmanager.chat_historys = progress['chat_historys']
|
||||
else:
|
||||
chatmanager.chat_historys = []
|
||||
|
||||
# 恢复当前章节链
|
||||
if 'chapter_chain_now' in progress:
|
||||
chatmanager.chapter_chain_now = progress['chapter_chain_now']
|
||||
else:
|
||||
chatmanager.chat_historys = []
|
||||
chatmanager.scores = []
|
||||
|
||||
fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name)
|
||||
user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager)
|
||||
@@ -77,6 +105,10 @@ class AgentNamespace(Namespace):
|
||||
backboard_manager.add_backboard(user_uuid, user_id, course_id, lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
|
||||
chatmanager.bb = backboard_manager.get_backboard(user_uuid)
|
||||
chatmanager.next_chapter()
|
||||
|
||||
# 如果有保存的进度,需要跳过已经完成的章节
|
||||
for _ in range(need_skip_chapters):
|
||||
chatmanager.next_chapter()
|
||||
self.chatmanager = chatmanager
|
||||
self.ase_client = user_uuid2ase_client[user_uuid]
|
||||
user_uuid2ase_client[user_uuid].register_on_connect_entry(
|
||||
@@ -155,7 +187,7 @@ class AgentNamespace(Namespace):
|
||||
if data['type'] == 'function': # 在这里 前端会发送允许执行的function与参数
|
||||
print("function_call", data['data'])
|
||||
d = data['data']
|
||||
res = chatmanager[uuid].function_call(d['data']['name'], d['data']['args'])
|
||||
res = chatmanager[uuid].function_call(d['data']['name'], d['data'].get('args', {}))
|
||||
d['data'] = res.to_dict()
|
||||
print("function_call_res", d['data'])
|
||||
emit(d['route'], d, room=uuid, namespace='/agent')
|
||||
@@ -179,6 +211,7 @@ class AgentNamespace(Namespace):
|
||||
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
|
||||
ase_client.chatmanager.load_next_chapter()
|
||||
ase_client.send_text("chapter-start", "")
|
||||
ase_client.send_text("clear","")
|
||||
namespace_entry.emit('message', "Multi-Agents服务已连接", room=init_data['room'], namespace='/agent')
|
||||
|
||||
#接受消息回来直接让message监听发送
|
||||
@@ -217,8 +250,10 @@ class AgentNamespace(Namespace):
|
||||
if len(chatmanager.scores) >= chatmanager.chapter_chain_now:
|
||||
chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None)
|
||||
else: chatmanager.scores.append(data.get('data', None))
|
||||
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.material_id, chatmanager.chapter_name, chatmanager.lesson_name)
|
||||
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.course_id, chatmanager.chapter_name, chatmanager.lesson_name)
|
||||
chatmanager.load_next_chapter()
|
||||
time.sleep(0.5)
|
||||
chatmanager.ase_client.send_text("chapter-start", "")
|
||||
|
||||
|
||||
def on_initiative(self,data): # 主动call
|
||||
@@ -234,12 +269,33 @@ class AgentNamespace(Namespace):
|
||||
emit('message', res.message, room=uuid, namespace='/agent')
|
||||
|
||||
|
||||
|
||||
def on_disconnect(self,data):
|
||||
print(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}')
|
||||
ex = current_app.extensions
|
||||
uuid = session.get('user_uuid')
|
||||
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
|
||||
user_uuid2chatmanager[uuid].disconnect(uuid)
|
||||
user_uuid2chatmanager = ex.get("user_uuid2chatmanager", {})
|
||||
|
||||
# 保存学习进度到MongoDB
|
||||
try:
|
||||
# 获取用户ID
|
||||
uuid2username = ex.get("uuid2username", {})
|
||||
user_id = uuid2username.get(uuid, "unknown")
|
||||
|
||||
# 调用保存学习进度功能
|
||||
chatmanager = user_uuid2chatmanager.get(uuid)
|
||||
if chatmanager:
|
||||
save_result = save_learning_progress(uuid, chatmanager, user_id)
|
||||
print(f"保存学习进度结果: {save_result}, user_uuid={uuid}")
|
||||
except Exception as e:
|
||||
print(f"保存学习进度时发生异常: {str(e)}")
|
||||
|
||||
# 执行原有的断开连接逻辑
|
||||
if uuid in user_uuid2chatmanager:
|
||||
user_uuid2chatmanager[uuid].disconnect(uuid)
|
||||
|
||||
# 离开房间
|
||||
if uuid:
|
||||
leave_room(uuid, namespace='/agent')
|
||||
|
||||
print("VSCode client disconnected")
|
||||
print("Disconnect reason:"+str(data))
|
||||
@@ -22,7 +22,30 @@
|
||||
<body>
|
||||
<h1>效率的重要性与实践验证</h1>
|
||||
<h2>效率</h2>
|
||||
<h3>理论热身:算法选择的智慧</h3>
|
||||
<h3>算法是什么</h3>
|
||||
<h4>算法就是解决某一个问题的做法,其实它在生活中无处不在</h4>
|
||||
<p>比如从学校宿舍走到食堂:</p>
|
||||
<ol>
|
||||
<li>要先宿舍下楼</li>
|
||||
<li>然后到食堂楼之间可能有3条路,
|
||||
A. 直线方向穿过曲折难走的小路,
|
||||
B. 先走远路到平坦的道上,
|
||||
C. 等一会校车,</li>
|
||||
<li>从3者选择一条走过去,最后再上楼。</li>
|
||||
</ol>
|
||||
<h4>算法的5大组成</h4>
|
||||
<ol>
|
||||
<li>输入</li>
|
||||
<li>输出</li>
|
||||
<li>有穷性</li>
|
||||
<li>确定性</li>
|
||||
<li>可行性</li>
|
||||
</ol>
|
||||
<p>这5大组成其实暗示了一个特性
|
||||
其实对于所有的可以被算法描述的问题,
|
||||
一定会有一种算法有解的。
|
||||
至少有一种方法称为”暴力搜索“”穷举法“,穷尽一切可能。</p>
|
||||
<h3>效率的重要性与验证</h3>
|
||||
<h4>任务:分析与决策</h4>
|
||||
<p>项目组目前有两套备选的交通信号灯同步算法,它们将在不同性能的服务器上运行:</p>
|
||||
<ul>
|
||||
@@ -34,7 +57,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p>你的项目经理(右侧的Agent)希望你通过分析,来判断哪个方案更具前景。请与TA对话,逐一回答以下问题。</p>
|
||||
<h4>思考题</h4>
|
||||
<h4>问题</h4>
|
||||
<p>与右侧的Agent对话,回答以下问题:</p>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -52,7 +75,8 @@
|
||||
</ol>
|
||||
<h3>编程实践:验证算法的真实性能</h3>
|
||||
<h4>任务:编码与分析</h4>
|
||||
<p>理论分析让你认识到了算法效率的重要性。现在,你需要通过编程来亲身感受不同交通状况对同一算法性能的巨大影响。我们将以“插入排序”为例,来处理三种典型的交通流量数据,这分别对应算法分析中的<strong>最好</strong>,<strong>最坏</strong>和<strong>平均</strong>情况。</p>
|
||||
<p>理论分析让你认识到了算法效率的重要性。现在,你需要通过编程来亲身感受不同交通状况对同一算法性能的巨大影响。
|
||||
我们将以“插入排序”为例,来处理三种典型的交通流量数据,这分别对应算法分析中的<strong>最好</strong>,<strong>最坏</strong>和<strong>平均</strong>情况。</p>
|
||||
<h5>题目:模拟交通流量排序</h5>
|
||||
<p>实现插入排序算法,并验证其在处理“畅通无阻”(数据有序)、“交通大堵塞”(数据逆序)和“随机车流”(数据随机)三种模式时的效率差异。</p>
|
||||
<h5>代码框架</h5>
|
||||
@@ -127,6 +151,27 @@ for size in network_sizes:
|
||||
<p><strong>融会贯通</strong>:结合第一关的理论分析和第二关的编程实验,你对“算法是解决问题的核心”这句话有了怎样更深的理解?</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>规模与增长率</h3>
|
||||
<h4>算法复杂度定义</h4>
|
||||
<p>为了更好地描述算法优化的效果,定义为当问题规模趋于无穷大时算法运行时间(算法复杂度,也可以理解为计算机运行的步骤数)</p>
|
||||
<p>符号:<code>Θ( f(n) )</code> 相对常用,
|
||||
称为“渐进等于”,表示算法复杂度随着问题规模n的增大而增大的速率,和函数f(n)在常数倍率上相同。</p>
|
||||
<h4>算法复杂度的计算</h4>
|
||||
<p>计算时间复杂度是一件比较重要的技能,我们来用一些例子试着计算:
|
||||
假设你是一个收银员,有n个人排队,1分钟你只能收银1个人,随着n增大,你收银的时间复杂度(时长)增长,和哪一个函数增长“渐进等于”呢?</p>
|
||||
<h5>增加一点难度</h5>
|
||||
<p>你这个收银员有超能力,可以越来越熟练,第1个人用时1分钟,第2人用时1/2分钟,第3人只用1/4分钟,随着n增大,时间复杂度怎么样呢?</p>
|
||||
<h5>再难一点</h5>
|
||||
<p>如果你这个超能力是这样的:第1个人用时1分钟,后面2个人用时1分钟,后面4个人用时1分钟,后面8个人用时1分钟,那么这种情况下,随着用户n的增大,时间复杂度可以用那个函数描述呢?</p>
|
||||
<h4>另外两个符号</h4>
|
||||
<p>最后还有两个符号:</p>
|
||||
<ul>
|
||||
<li>O 记号:渐近 “小于”:f (n)“≤”g (n)</li>
|
||||
<li>Ω 记号:渐近 “大于”:f (n)“≥”g (n)
|
||||
比如上面Θ( n ) > Θ( log_2(n) ) > Θ( 1 ),就可以写作Θ( 1 )= O ( log_2(n) ) = O ( n ),或者写作Θ( n ) = Ω( log_2(n) ) = Ω( 1 )</li>
|
||||
</ul>
|
||||
<p>当然事实上,上面的写法比较不常见,只是让大家理解一下,常函数的增长渐进小于log_2(n),也渐进小于n。</p>
|
||||
<p>在具体的使用中,由于渐进大于没什么意义(我们不会去找一个更差的算法),我们常混用Θ、O,也就是只研究函数上界(研究一个算法复杂度渐进小于哪一个函数)</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>效率的重要性与实践验证</h1>
|
||||
<h2>效率</h2>
|
||||
<h3>算法是什么</h3>
|
||||
<h4>算法就是解决某一个问题的做法,其实它在生活中无处不在</h4>
|
||||
<p>比如从学校宿舍走到食堂:</p>
|
||||
<ol>
|
||||
<li>要先宿舍下楼</li>
|
||||
<li>然后到食堂楼之间可能有3条路,
|
||||
A. 直线方向穿过曲折难走的小路,
|
||||
B. 先走远路到平坦的道上,
|
||||
C. 等一会校车,</li>
|
||||
<li>从3者选择一条走过去,最后再上楼。</li>
|
||||
</ol>
|
||||
<h4>算法的5大组成</h4>
|
||||
<ol>
|
||||
<li>输入</li>
|
||||
<li>输出</li>
|
||||
<li>有穷性</li>
|
||||
<li>确定性</li>
|
||||
<li>可行性</li>
|
||||
</ol>
|
||||
<p>这5大组成其实暗示了一个特性
|
||||
其实对于所有的可以被算法描述的问题,
|
||||
一定会有一种算法有解的。
|
||||
至少有一种方法称为”暴力搜索“”穷举法“,穷尽一切可能。</p>
|
||||
<h3>效率的重要性与验证</h3>
|
||||
<h4>任务:分析与决策</h4>
|
||||
<p>项目组目前有两套备选的交通信号灯同步算法,它们将在不同性能的服务器上运行:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>方案A</strong>:部署在超级服务器上(10^9 次运算/秒),采用的是一种较为简单的算法,处理n个路口需要 <eq>2n^2</eq> 次计算。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>方案B</strong>:部署在普通服务器上(10^7 次运算/秒),但采用的是一种更优化的算法,处理n个路口需要 <eq>50nlog_2n</eq> 次计算。</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>你的项目经理(右侧的Agent)希望你通过分析,来判断哪个方案更具前景。请与TA对话,逐一回答以下问题。</p>
|
||||
<h4>问题</h4>
|
||||
<p>与右侧的Agent对话,回答以下问题:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>概念回顾</strong>:首先,请向你的“经理”解释,根据课程所学,一个合格的“算法”应具备哪些基本特征?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>小规模测试</strong>:对于一个包含100个路口的小型城区(n=100),计算并说明方案A和方案B分别需要多长时间完成计算?在这种情况下,你会推荐哪个方案?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>大规模应用</strong>:现在,我们需要为一座拥有100万个路口的大都市(n=1,000,000)进行规划。再次计算并说明两个方案的耗时。你的推荐会改变吗?为什么?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>总结陈词</strong>:综合以上分析,向你的“经理”总结:为什么一个更优的算法设计,其重要性远超硬件性能的提升? 这验证了课程中提到的哪个核心观点?</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>编程实践:验证算法的真实性能</h3>
|
||||
<h4>任务:编码与分析</h4>
|
||||
<p>理论分析让你认识到了算法效率的重要性。现在,你需要通过编程来亲身感受不同交通状况对同一算法性能的巨大影响。
|
||||
我们将以“插入排序”为例,来处理三种典型的交通流量数据,这分别对应算法分析中的<strong>最好</strong>,<strong>最坏</strong>和<strong>平均</strong>情况。</p>
|
||||
<h5>题目:模拟交通流量排序</h5>
|
||||
<p>实现插入排序算法,并验证其在处理“畅通无阻”(数据有序)、“交通大堵塞”(数据逆序)和“随机车流”(数据随机)三种模式时的效率差异。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在代码编辑区,完成 <code>insertion_sort(arr)</code> 函数的实现后,运行完整代码,并与Agent讨论结果。
|
||||
<strong>请创建任意文件,将下面代码写入到编辑器中</strong></p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def insertion_sort(arr):
|
||||
"""
|
||||
实现插入排序算法
|
||||
参数arr: 待排序的交通数据数组(整数代表车辆通行次序)
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
# TODO: 请在此处实现你的插入排序逻辑
|
||||
|
||||
def generate_traffic_data(n):
|
||||
"""
|
||||
生成模拟交通数据
|
||||
参数n: 数据规模(路口数量或监控点数量)
|
||||
返回: 三种不同交通状况的数据
|
||||
"""
|
||||
random_data = [random.randint(0, 10**6) for _ in range(n)]
|
||||
# 模拟“畅通无阻”:交通流按次序进入,数据基本有序 (Best Case)
|
||||
best_case_data = sorted(random_data)
|
||||
# 模拟“交通大堵塞”:疏散时情况完全反转,数据逆序 (Worst Case)
|
||||
worst_case_data = sorted(random_data, reverse=True)
|
||||
# 模拟“随机车流”:正常但无规律的交通状况 (Average Case)
|
||||
average_case_data = random_data
|
||||
return best_case_data, worst_case_data, average_case_data
|
||||
|
||||
def measure_performance(func, data):
|
||||
"""
|
||||
测量算法性能
|
||||
参数func: 排序函数
|
||||
参数data: 交通数据
|
||||
返回: 执行时间(毫秒)
|
||||
"""
|
||||
start_time = time.perf_counter_ns()
|
||||
func(data.copy()) # 使用副本避免影响其他测试
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6 # 转换为毫秒
|
||||
|
||||
#测试不同规模的路口网络
|
||||
network_sizes = [1000, 5000, 10000]
|
||||
print("交通数据处理算法性能测试:")
|
||||
for size in network_sizes:
|
||||
best, worst, avg = generate_traffic_data(size)
|
||||
|
||||
time_best = measure_performance(insertion_sort, best)
|
||||
time_worst = measure_performance(insertion_sort, worst)
|
||||
time_avg = measure_performance(insertion_sort, avg)
|
||||
|
||||
print(f"网络规模 n={size}:")
|
||||
print(f" - 畅通无阻 (Best Case): {time_best:.2f} ms")
|
||||
print(f" - 交通大堵塞 (Worst Case): {time_worst:.2f} ms")
|
||||
print(f" - 随机车流 (Average Case): {time_avg:.2f} ms")
|
||||
</code></pre>
|
||||
<h4>分析与讨论</h4>
|
||||
<p>完成编程并得到输出后,请与右侧Agent讨论以下问题,以检验你的理解:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>结果分析</strong>:当网络规模从1000增加到10000时,“交通大堵塞”(最坏情况)的处理时间增长了大约多少倍?这更符合O(n)(线性)还是O(n2)(二次)的增长模式?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>原因探究</strong>:为什么“畅通无阻”(最好情况)的处理速度如此之快?它的时间复杂度是什么?请结合你的代码逻辑来解释。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>实践应用</strong>:根据你的实验结果,你认为插入排序是否适合用于需要快速响应大规模交通拥堵的实时预警系统?为什么?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>融会贯通</strong>:结合第一关的理论分析和第二关的编程实验,你对“算法是解决问题的核心”这句话有了怎样更深的理解?</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>规模与增长率</h3>
|
||||
<h4>算法复杂度定义</h4>
|
||||
<p>为了更好地描述算法优化的效果,定义为当问题规模趋于无穷大时算法运行时间(算法复杂度,也可以理解为计算机运行的步骤数)</p>
|
||||
<p>符号:<code>Θ( f(n) )</code> 相对常用,
|
||||
称为“渐进等于”,表示算法复杂度随着问题规模n的增大而增大的速率,和函数f(n)在常数倍率上相同。</p>
|
||||
<h4>算法复杂度的计算</h4>
|
||||
<p>计算时间复杂度是一件比较重要的技能,我们来用一些例子试着计算:
|
||||
假设你是一个收银员,有n个人排队,1分钟你只能收银1个人,随着n增大,你收银的时间复杂度(时长)增长,和哪一个函数增长“渐进等于”呢?</p>
|
||||
<h5>增加一点难度</h5>
|
||||
<p>你这个收银员有超能力,可以越来越熟练,第1个人用时1分钟,第2人用时1/2分钟,第3人只用1/4分钟,随着n增大,时间复杂度怎么样呢?</p>
|
||||
<h5>再难一点</h5>
|
||||
<p>如果你这个超能力是这样的:第1个人用时1分钟,后面2个人用时1分钟,后面4个人用时1分钟,后面8个人用时1分钟,那么这种情况下,随着用户n的增大,时间复杂度可以用那个函数描述呢?</p>
|
||||
<h4>另外两个符号</h4>
|
||||
<p>最后还有两个符号:</p>
|
||||
<ul>
|
||||
<li>O 记号:渐近 “小于”:f (n)“≤”g (n)</li>
|
||||
<li>Ω 记号:渐近 “大于”:f (n)“≥”g (n)
|
||||
比如上面Θ( n ) > Θ( log_2(n) ) > Θ( 1 ),就可以写作Θ( 1 )= O ( log_2(n) ) = O ( n ),或者写作Θ( n ) = Ω( log_2(n) ) = Ω( 1 )</li>
|
||||
</ul>
|
||||
<p>当然事实上,上面的写法比较不常见,只是让大家理解一下,常函数的增长渐进小于log_2(n),也渐进小于n。</p>
|
||||
<p>在具体的使用中,由于渐进大于没什么意义(我们不会去找一个更差的算法),我们常混用Θ、O,也就是只研究函数上界(研究一个算法复杂度渐进小于哪一个函数)</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
32
Html/apps/static/68bacdfadf5aeae0912f7f18-第七周-新7周课时.html
Normal file
32
Html/apps/static/68bacdfadf5aeae0912f7f18-第七周-新7周课时.html
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>新7周课时</h1>
|
||||
<h2>阶段一:示例阶段</h2>
|
||||
<h3>步骤A</h3>
|
||||
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
|
||||
<h3>步骤B</h3>
|
||||
<p>继续补充该阶段的其它步骤内容。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第三周-分治法实践与优化.html
Normal file
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第三周-分治法实践与优化.html
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>分治策略进阶与主方法</h1>
|
||||
<blockquote>
|
||||
<p>Auto-generated at 2025-09-22 15:44</p>
|
||||
</blockquote>
|
||||
<h2>分治策略进阶与主方法</h2>
|
||||
<h3>最大子数组问题</h3>
|
||||
<h4>任务:编码与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
|
||||
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>,总增量最大的连续时段是从第8天到第11天,总增量为 <code>18 + 20 - 7 + 12 = 43</code> 。
|
||||
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
|
||||
<h5>题目:最大交通流量增量</h5>
|
||||
<p>你需要实现一个分治算法来解决最大子数组问题。
|
||||
同时,为了对比,你也会实现一个暴力求解算法。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
|
||||
<pre><code class="language-python">import time
|
||||
import random
|
||||
|
||||
def find_maximum_subarray_brute(arr):
|
||||
"""
|
||||
使用暴力法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
|
||||
#--- 本周任务:请在下方实现分治算法 ---
|
||||
|
||||
def find_max_crossing_subarray(arr, low, mid, high):
|
||||
"""
|
||||
找到跨越中点的最大子数组
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现寻找跨越中点的最大子数组的逻辑
|
||||
# 提示: 从mid向左和向右分别扫描,找到各自的最大和
|
||||
|
||||
def find_maximum_subarray(arr, low, high):
|
||||
"""
|
||||
使用分治法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现分治递归逻辑
|
||||
# 提示: 递归基是当数组只有一个元素时
|
||||
|
||||
#--- 测试与对比部分 --- 以下内容无需修改
|
||||
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
|
||||
print(f"交通流量变化数据: {traffic_changes}")
|
||||
|
||||
#使用分治法
|
||||
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
|
||||
print(f"分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}")
|
||||
|
||||
#使用暴力法验证
|
||||
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
|
||||
print(f"暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}")
|
||||
|
||||
#性能测试
|
||||
large_data = [random.randint(-50, 50) for _ in range(2000)]
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray(large_data, 0, len(large_data) - 1)
|
||||
dc_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms")
|
||||
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray_brute(large_data)
|
||||
brute_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms")
|
||||
</code></pre>
|
||||
<h3>分治经典算法:快速排序</h3>
|
||||
<h4>快速排序原理</h4>
|
||||
<p>快速排序(Quick Sort)是分治法的经典应用之一。与归并排序不同的是,快速排序通过原地划分数组来完成排序,无需额外的同规模辅助存储空间。它在平均情况下表现极为高效,是实际应用中常用的排序算法之一。</p>
|
||||
<p>快速排序的分治核心思路非常好理解,对于一个未排序的数组,希望分解为左区间和一个“枢轴(pivot)”元素和右区间,其中左区间的数都小于等于枢轴,右区间的都大于等于枢轴。
|
||||
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
|
||||
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
|
||||
<h4>任务:编码与实验</h4>
|
||||
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
|
||||
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
|
||||
代码框架
|
||||
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
实现快速排序算法(固定枢轴策略)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
# TODO: 实现快速排序的分治逻辑
|
||||
# 提示:选取第一个元素为枢轴,递归排序左右子数组
|
||||
|
||||
def measure_performance(sort_func, data):
|
||||
start_time = time.perf_counter_ns()
|
||||
sort_func(data.copy()) # 对数据副本排序,避免影响原数据
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6 # 转换为毫秒
|
||||
|
||||
#性能测试:对比有序输入和随机输入
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("快速排序性能测试(固定枢轴):")
|
||||
for n in sizes:
|
||||
sorted_data = list(range(n))
|
||||
random_data = [random.randint(0, 10**6) for _ in range(n)]
|
||||
time_sorted = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(quick_sort, random_data)
|
||||
print(f"数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms")
|
||||
</code></pre>
|
||||
<p>完成编码并运行测试,报告时间差异。</p>
|
||||
<h3>快速排序的复杂度分析</h3>
|
||||
<h4>时间复杂度</h4>
|
||||
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong>和</strong>递归深度</strong>共同决定
|
||||
三种典型场景:最坏情况、平均情况、最佳情况</p>
|
||||
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
|
||||
平均情况:输入随机 / 等概率选枢轴
|
||||
最佳情况:每次划分平分数组</p>
|
||||
<h4>空间复杂度</h4>
|
||||
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
|
||||
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
|
||||
<h4>分析</h4>
|
||||
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
|
||||
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
|
||||
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
|
||||
<h4>实践</h4>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def random_quick_sort(arr):
|
||||
"""
|
||||
实现随机快速排序算法(随机选择枢轴)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
你在上一章实现的快排内容
|
||||
"""
|
||||
|
||||
#验证随机快排在极端有序输入下的性能
|
||||
n = 10000
|
||||
sorted_data = list(range(n))
|
||||
time_fixed = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(random_quick_sort, sorted_data)
|
||||
print(f"数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms")
|
||||
|
||||
</code></pre>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-比较型排序.html
Normal file
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-比较型排序.html
Normal file
@@ -0,0 +1,159 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>基于比较的排序</h1>
|
||||
<h2>基于比较的排序</h2>
|
||||
<h3>优先队列的原理</h3>
|
||||
<p><strong>优先队列(Priority Queue)</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
|
||||
<br>
|
||||
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
|
||||
<br><br>
|
||||
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
|
||||
<br><br>
|
||||
<strong>堆(Heap)结构</strong>是实现优先队列的首选。</p>
|
||||
<h3>数组模拟堆实现的优先队列</h3>
|
||||
<h4>堆结构与取存原理</h4>
|
||||
<p>堆(Heap)本质上是一个完全二叉树结构:
|
||||
(当然也可以是多叉树,但没有必要)
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1:大根堆的二叉树结构图示" /></p>
|
||||
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
|
||||
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
|
||||
<h5>取数</h5>
|
||||
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
|
||||
但是将这个数取出后还不能结束,因为需要维护堆的性质。
|
||||
<br>
|
||||
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
|
||||
<br></p>
|
||||
<h5>存数</h5>
|
||||
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
|
||||
<h4>数组模拟堆</h4>
|
||||
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2:左边是堆的数组表示,右边是其对应的二叉树" />
|
||||
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
|
||||
<p>数组下标从1开始,任意一个下标i,其左右儿子下标刚好就是"i*2"和“i*2+1”。这样在后续代码实现时,代码写起来就会简单很多。</p>
|
||||
<h6>建堆</h6>
|
||||
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
|
||||
对于一个元素随机的数组,只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
|
||||
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
|
||||
<h3>优先队列的算法实现</h3>
|
||||
<h4>练习:堆操作</h4>
|
||||
<p>在进行代码实现之前,做一个问题练习:
|
||||
对于一个随机队列"[3, 32, 6, 43, 5, 8, 0, 9]",经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
|
||||
将答案告知AI教师。</p>
|
||||
<h4>任务:城市事件优先调度</h4>
|
||||
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
|
||||
<p><br><br></p>
|
||||
<h4>题目:实现堆排序</h4>
|
||||
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
|
||||
<br>
|
||||
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
|
||||
<br>
|
||||
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
|
||||
<br></p>
|
||||
<h5>代码框架</h5>
|
||||
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def max_heapify(arr, n, i):
|
||||
"""
|
||||
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
|
||||
调整结点 i 使以其为根的子树成为最大堆
|
||||
参数:
|
||||
arr: 存储堆的数组
|
||||
n: 堆的有效大小(长度)
|
||||
i: 需要下滤调整的节点索引
|
||||
"""
|
||||
# TODO: 在此处实现 "下滤" 操作,将 arr[i] 下沉到正确位置
|
||||
|
||||
|
||||
def build_max_heap(arr):
|
||||
"""
|
||||
将无序数组原地建成最大堆,从后往前进行下滤
|
||||
参数:
|
||||
arr: 待调整的数组
|
||||
"""
|
||||
# TODO: 调用 max_heapify 将 arr 调整为堆
|
||||
|
||||
|
||||
def heap_sort(arr):
|
||||
"""
|
||||
利用堆排序算法排序数组(降序)
|
||||
参数:
|
||||
arr: 待排序数组
|
||||
返回:
|
||||
排序后的数组(从大到小)
|
||||
"""
|
||||
# TODO: 完成堆排序的实现
|
||||
n = len(arr)
|
||||
# 1. 原地建堆
|
||||
build_max_heap(arr)
|
||||
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
|
||||
|
||||
|
||||
#性能测试:对比不同规模输入的堆排序用时
|
||||
def measure(sort_func, data):
|
||||
start = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end = time.perf_counter_ns()
|
||||
return (end - start) / 10**6 # 毫秒
|
||||
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("堆排序性能测试:")
|
||||
for n in sizes:
|
||||
data = [random.randint(0, 1000000) for _ in range(n)]
|
||||
t = measure(heap_sort, data)
|
||||
print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>实验结果分析</h5>
|
||||
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>,当输入规模增大时,运行时间应呈现近似线性乘以对数的增长趋势。具体来说,若将输入规模扩大10倍,运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
|
||||
<br>
|
||||
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比,你会发现堆排序对规模扩大的响应增长显著缓慢得多。
|
||||
<br>
|
||||
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
|
||||
<br>
|
||||
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
|
||||
<h3>比较排序的决策树模型</h3>
|
||||
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
|
||||
<br>
|
||||
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>。
|
||||
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
|
||||
<p>决策树是描述比较排序过程的一种抽象模型。
|
||||
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
|
||||
<br>
|
||||
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
|
||||
<br>
|
||||
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
|
||||
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
|
||||
<br>
|
||||
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
|
||||
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
|
||||
<br>
|
||||
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
|
||||
<br>
|
||||
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
|
||||
<br>
|
||||
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>,因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符:对三个无任何特殊性质的数进行排序,最少需要3次比较才能确定它们的正确顺序。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
219
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-非比较型排序.html
Normal file
219
Html/apps/static/68bacdfadf5aeae0912f7f18-第三章:排序-非比较型排序.html
Normal file
@@ -0,0 +1,219 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>非比较型排序</h1>
|
||||
<h2>非比较型排序</h2>
|
||||
<h3>基数排序原理</h3>
|
||||
<h4>排序算法复杂度背景</h4>
|
||||
<p>基于比较的排序算法,理论上无法突破(Ω(n \log n))时间复杂度下界
|
||||
特殊场景下,不依赖元素间比较、利用元素结构特征,可实现线性时间排序</p>
|
||||
<h4>基数排序核心思想</h4>
|
||||
<p>将元素视为由多个 “位”(digit)组成
|
||||
按位进行多轮排序,每轮针对一个关键位
|
||||
依赖稳定的子排序算法,保证先前排好的顺序不被打乱
|
||||
关键保障:<strong>排序的稳定性(相同元素相对顺序不变)</strong></p>
|
||||
<h4>经典示例(十进制整数的基数排序):</h4>
|
||||
<p>按最低有效位(LSB)→ 最高有效位(MSB) 逐轮排序
|
||||
第一轮:按个位数排序(0-9 分组)
|
||||
后续轮次:依次按十位数、百位数等排序</p>
|
||||
<h4>实操流程:</h4>
|
||||
<p>初始序列:[329, 457, 657, 839, 436]
|
||||
第一轮(按个位排序):
|
||||
按个位 0-9 分组 → [436(6), 457(7), 657(7), 329(9), 839(9)]
|
||||
(注:457 与 657 个位相同,保持原顺序)
|
||||
第二轮(按十位排序):
|
||||
按十位 0-9 分组 → [329(2), 436(3), 839(3), 457(5), 657(5)]
|
||||
(注:436 与 839 十位相同,保持第一轮后的顺序)
|
||||
第三轮(按百位排序):
|
||||
按百位 0-9 分组 → [329(3), 436(4), 457(4), 657(6), 839(8)]
|
||||
(注:436 与 457 百位相同,保持第二轮后的顺序)</p>
|
||||
<p>每轮排序通过稳定性保留前序结果,最终实现 “高位决定整体顺序”,完成正确排序。</p>
|
||||
<h4>时间复杂度:</h4>
|
||||
<p>每轮扫一遍序列O(n),总共轮数就是基数位数,设为k。
|
||||
时间复杂度为O(kn),其中k一般不太大。</p>
|
||||
<h3>基数排序的实现</h3>
|
||||
<p>为了加深对基数排序的理解,我们通过一个编程任务来实践其实现。我们将以整数排序为例,并采用从最低有效位开始的方法对数字进行排序(LSD 基数排序)。</p>
|
||||
<p>在实现过程中,我们需要编写一个按某个位数进行计数排序的辅助函数,然后在主函数中对每一位循环调用该辅助函数。</p>
|
||||
<h4>用计数排序稳定地对基数排序</h4>
|
||||
<p>每选定一个基数后,需要用O(n)的时间复杂度将当前序列按照这个基数进行排序;或说<strong>找到按照当前基数下,当前序列的每一个数要前往的位置</strong>
|
||||
这里用“计数排序”这个算法。
|
||||
具体做法如下:</p>
|
||||
<ol>
|
||||
<li>建一个新数组count,记录当前基数的每个不同的数有多少个(计数)</li>
|
||||
<li>从前往后遍历序列,取出序列每一个元素在当前基数位置上的值,对应计数值+1(count[number]+=1)</li>
|
||||
<li>将count转变为前缀和,这样就得到了每一个基数对应的原始数的最后一个,在新序列中的位置</li>
|
||||
<li>从后往前遍历原序列,将每一个数(基数上的数为number)放到对应新序列count[number]-1的位置,并让count[number]-=1。</li>
|
||||
</ol>
|
||||
<h5>练习:按位计数排序</h5>
|
||||
<p>作为练习,[170, 45, 75, 90, 802, 24, 2, 66],按个位排序(exp=1)进行计数排序,请写下count数组的计数结果(2.步后)和前缀和结果。并表述在第4步的利用方式,从而得到序列结果。</p>
|
||||
<h4>任务:实现 LSD 基数排序</h4>
|
||||
<p>请实现 radix_sort(arr) 函数,对传入的整数列表进行从小到大的排序。你可以假设所有整数为非负且位数相对固定。为了简化问题,我们以十进制为基数(基数=10)实现,并假定输入整数的最大位数为<eq>d</eq>(例如<eq>10^d</eq>数量级)。算法思路如下:</p>
|
||||
<p>从最低位(个位,exp=1)开始,对数组进行计数排序(Counting Sort),按该位的数值对元素排序。</p>
|
||||
<p>然后依次向更高一位(exp=10,exp=100,...)进行排序,直到处理完最高位。</p>
|
||||
<h4>代码框架</h4>
|
||||
<p>请在下方代码编辑区完成 counting_sort_by_digit 和 radix_sort 函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def counting_sort_by_digit(arr, exp):
|
||||
"""
|
||||
对数组按某个数位进行计数排序(稳定的)。
|
||||
参数:
|
||||
arr: 要排序的数组
|
||||
exp: 位标志,表示当前要按照 (exp位) 上的数值进行排序,
|
||||
例如 exp=1 表示按个位排序,exp=10 表示按十位排序。
|
||||
返回:
|
||||
按指定位排序后的数组副本
|
||||
"""
|
||||
n = len(arr)
|
||||
output = [0] * n # 存放排序结果
|
||||
count = [0] * 10 # 计数数组(十进制位范围0-9)
|
||||
# TODO: 计算每个数组元素在该位上的值,并计数
|
||||
|
||||
|
||||
def radix_sort(arr):
|
||||
"""
|
||||
基数排序(按从低位到高位逐位排序)
|
||||
参数:
|
||||
arr: 待排序的整数列表
|
||||
返回:
|
||||
按非递减顺序排序的列表
|
||||
"""
|
||||
#TODO: 利用 counting_sort_by_digit 按位排序数组
|
||||
|
||||
|
||||
#测试与性能分析
|
||||
def measure(sort_func, data):
|
||||
start = time.perf_counter_ns()
|
||||
sort_func(data)
|
||||
end = time.perf_counter_ns()
|
||||
return (end - start) / 10**6 # 转换为毫秒
|
||||
|
||||
#生成随机测试数据
|
||||
sizes = [10000, 50000, 100000]
|
||||
print("基数排序性能测试:")
|
||||
for n in sizes:
|
||||
data = [random.randrange(0, 1000000) for _ in range(n)]
|
||||
t = measure(radix_sort, data)
|
||||
print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")
|
||||
</code></pre>
|
||||
<p>性能讨论</p>
|
||||
<p>在完成代码并运行测试后,可以看到基数排序在不同规模输入下运行时间增长相对温和。理想情况下,对于固定位数<eq>d</eq>的整数,基数排序的时间复杂度为<eq>O(d \times (n + k))</eq>,其中<eq>k</eq>为每位可能的取值数量(对十进制整数而言<eq>k=10</eq>)。
|
||||
<br>
|
||||
当<eq>d</eq>视作常数时,复杂度近似<eq>O(n)</eq>,因此输入规模扩大倍数时,运行时间应近似线性增长。
|
||||
<br>
|
||||
然而需注意,基数排序的实际常数因子不小(多次稳定排序和额外空间),对中等规模数据Python实现未必比C语言内建排序更快。
|
||||
<br>
|
||||
更重要的是,基数排序并非万能:如果元素位数<eq>d</eq>随<eq>n</eq>增长(例如排序<eq>1</eq>到<eq>N</eq>范围的数,<eq>N</eq>约为<eq>n</eq>量级),则<eq>d = O(\log n)</eq>,此时基数排序复杂度<eq>≈O(n \log n)</eq>,并没有打破比较排序下界。因此,基数排序的线性性能依赖于位数相对较小这一前提。</p>
|
||||
<h3>桶排序的原理与实现</h3>
|
||||
<h4>桶排序核心定位</h4>
|
||||
<p>典型的线性时间排序算法
|
||||
利用输入数据的<strong>分布特性(假设大致均匀分布在某范围)</strong></p>
|
||||
<h4>桶排序原理与类比</h4>
|
||||
<h5>核心逻辑(三步):</h5>
|
||||
<p>第一步:创建若干 “桶”,将元素按值映射到对应桶中(实现粗分类)
|
||||
第二步:对每个桶内元素单独排序(可选用任意排序算法)
|
||||
第三步:按桶的顺序合并所有元素,得到有序序列
|
||||
<br>
|
||||
映射:将小数按值投入对应桶,每个桶仅覆盖原始区间 1/10,数据量少
|
||||
桶内排序:因元素少,插入排序等简单算法足够快
|
||||
合并:按桶序号收集,因 “i<j 时,第 i 号桶元素<第 j 号桶元素”,天然有序
|
||||
<br></p>
|
||||
<h5>案例练习</h5>
|
||||
<p>使用桶排序算法对以下 8 个数字进行排序
|
||||
[35, 12, 48, 27, 5, 39, 18, 42]
|
||||
以48为分母,将其他所有数划分到0-0.2-0.4-0.6-0.8-1这5个桶中,输出每个桶里面的数字是什么。
|
||||
<br><br></p>
|
||||
<h4>代码实现</h4>
|
||||
<p>请实现 bucket_sort(arr) 函数,将传入的[0,1)区间的小数数组排序。思路如下:
|
||||
<br>
|
||||
创建若干个空桶(列表),桶的数量可以根据数据规模n选择,这里取<eq>n/10</eq>(为整数)个桶。
|
||||
<br>
|
||||
将每个元素按照其值乘以桶数量后的整数部分,映射到对应的桶中。例如值为0.23、桶数为10时,<eq>0.23 \times 10 = 2.3</eq>,放入索引2号桶。
|
||||
<br>
|
||||
对每个非空桶内部进行排序。你可以直接使用 Python 内置排序(sorted)或实现简单排序算法。</p>
|
||||
<p>按桶的顺序依次合并桶内元素,得到排序后的结果。</p>
|
||||
<h5>代码框架</h5>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def bucket_sort(arr):
|
||||
"""
|
||||
桶排序(适用于0到1区间的小数)
|
||||
参数:
|
||||
arr: 介于[0,1)的浮点数列表
|
||||
返回:
|
||||
升序排序后的列表
|
||||
"""
|
||||
# TODO: 按原理实现桶排序
|
||||
n = len(arr)
|
||||
if n == 0:
|
||||
return arr
|
||||
# 1. 创建桶
|
||||
|
||||
# 2. 分配元素到各桶
|
||||
|
||||
# 3. 桶内排序
|
||||
|
||||
|
||||
#简单测试与性能评估
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("桶排序性能测试:")
|
||||
for n in sizes:
|
||||
#生成n个[0,1)均匀分布的小数
|
||||
data = [random.random() for _ in range(n)]
|
||||
start = time.perf_counter_ns()
|
||||
sorted_data = bucket_sort(data)
|
||||
end = time.perf_counter_ns()
|
||||
elapsed = (end - start) / 10**6
|
||||
#验证排序正确性
|
||||
is_correct = "√" if sorted_data == sorted(data) else "×"
|
||||
print(f"数据规模 n={n}: 排序耗时 {elapsed:.2f} ms, 排序正确性 {is_correct}")
|
||||
|
||||
</code></pre>
|
||||
<h4>桶排序时间复杂度分析</h4>
|
||||
<h5>平均情况(理想分布):</h5>
|
||||
<p>前提:n 个元素、m 个桶,数据均匀分布 → 每个桶平均含 n/m 个元素
|
||||
优化:选 m 接近 n 的量级 → 桶内排序成本视为常数
|
||||
结果:整体期望运行时间为O(n)(线性时间)</p>
|
||||
<h5>最坏情况(分布不均):</h5>
|
||||
<p>极端场景:所有元素落入同一个桶
|
||||
复杂度:取决于桶内排序算法(O (n log n) 或 O (n²))+ 分配桶的 O (n)
|
||||
结果:退化至O (n log n) 甚至更差,失去线性时间优势
|
||||
结论:理想分布下性能出众,数据分布不均时无优势</p>
|
||||
<h3>桶排序与哈希</h3>
|
||||
<h4>核心相通点:映射分散思想</h4>
|
||||
<p>通过函数映射将元素分散到不同 “容器”(桶排序的 “桶”/ 哈希算法的 “哈希桶”),把原问题拆解为小的子问题
|
||||
<br>
|
||||
桶排序:用映射函数(如 index = int(num * bucket_count))划分元素所属桶,依据元素值分配
|
||||
哈希算法:用哈希函数计算键的哈希桶索引,确定元素存储位置
|
||||
这种映射也意味着都对<strong>分布均匀性的依赖</strong></p>
|
||||
<h4>关键区别:映射函数的目标差异</h4>
|
||||
<p>对比维度
|
||||
桶排序的映射函数:一般直接做除法:保留元素大小顺序(按值范围划分)
|
||||
哈希算法的哈希函数:随机映射函数,实现元素均匀分布,不能用于排序(除非特殊设计有序哈希)</p>
|
||||
<h4>总结</h4>
|
||||
<p>桶排序可看作哈希思想在排序问题中的应用:均通过 “先分类、后处理” 的范式优化性能,但因目标不同,映射函数设计存在本质差异。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>算法设计范式:分而治之与归并排序</h1>
|
||||
<blockquote>
|
||||
<p>Auto-generated at 2025-09-22 15:38</p>
|
||||
</blockquote>
|
||||
<h2>算法设计范式:分而治之与归并排序</h2>
|
||||
<h3>分治法:分而治之</h3>
|
||||
<h4>引入</h4>
|
||||
<p>在算法分析与设计中,有一些设计算法解决问题的通用方法可供大家选取尝试,其中一个很常用的方法就叫做“分治法”,字如其意,
|
||||
分治法就是将大问题分解为小问题(或说子问题),并将子问题持续分解下去,直到子问题可以用常数时间进行求解。
|
||||
最基础的问题:何时可以用分治法?</p>
|
||||
<ol>
|
||||
<li>找最值问题</li>
|
||||
<li>回文字符串</li>
|
||||
</ol>
|
||||
<h4>分治法的组成</h4>
|
||||
<p>分治法有3个重要组成:分解、解决、合并。</p>
|
||||
<p>有一些帮助我们进行这些操作的分析与设计原则:</p>
|
||||
<ol>
|
||||
<li><strong>子问题相似</strong>:分解出的子问题与原问题类型一致,可复用相同解法,保证递归或分治逻辑的一致性。</li>
|
||||
<li><strong>子问题互不干扰</strong>:子问题边界清晰、数据独立,避免相互影响,简化问题拆解与求解逻辑。</li>
|
||||
<li><strong>合并代价可控</strong>:子问题解合并为原问题解的时间 / 空间开销在可接受范围,避免抵消分治带来的效率提升。</li>
|
||||
</ol>
|
||||
<h4>分治法的时间复杂度迭代公式</h4>
|
||||
<p>T(n)= 子问题数量 * T( 子问题规模 ) + 合并这些子问题的解的用时</p>
|
||||
<h3>归并算法理论分析</h3>
|
||||
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
|
||||
<p>在上周的工作中,你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错,但在应对大规模拥堵(逆序数据)时,其性能瓶颈非常明显。本周,你的项目经理(Agent)将向你介绍一种更高效的算法设计思想——“分而治之”,并要求你掌握其代表算法:归并排序。</p>
|
||||
<h4>任务:理解与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>归并排序(Merge Sort)是解决大规模排序问题的利器。在投入编码之前,你必须先从理论上理解它为何如此高效。</p>
|
||||
<h5>思考题</h5>
|
||||
<p>请与右侧的Agent(你的项目经理)对话,清晰地回答以下问题,以证明你已理解核心思想:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>分治思想</strong>:根据课程资料,请解释什么是“分而治之”(Divide-and-Conquer)设计范式?它包含哪三个基本步骤?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
|
||||
<ul>
|
||||
<li><code>2</code> 代表什么?</li>
|
||||
<li><code>T(n/2)</code> 代表什么?</li>
|
||||
<li><code>Θ(n)</code> 代表什么?</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>效率对比</strong>:你的同事认为,插入排序经过极致优化后,处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时(即n趋于无穷大时),哪个算法最终会胜出?请从渐进增长的角度解释你的理由。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
|
||||
<ul>
|
||||
<li><eq>n^2</eq> (插入排序最坏情况)</li>
|
||||
<li>nlogn (归并排序)</li>
|
||||
<li>n (一种非常低效的蛮力算法)</li>
|
||||
<li><eq>2^n</eq> (另一种指数级算法)</li>
|
||||
<li>logn (类似二分查找的效率)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>归并排序的编程实现</h3>
|
||||
<h4>任务:编码与对比</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
|
||||
<h5>题目:实现归并排序并对比性能</h5>
|
||||
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code> 和 <code>merge(left, right)</code> 两个函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
#上周实现的插入排序(用于对比)
|
||||
def insertion_sort(arr):
|
||||
for i in range(1, len(arr)):
|
||||
key = arr[i]
|
||||
j = i - 1
|
||||
while j >= 0 and arr[j] > key:
|
||||
arr[j + 1] = arr[j]
|
||||
j -= 1
|
||||
arr[j + 1] = key
|
||||
return arr
|
||||
|
||||
#--- 本周任务:请在下方实现归并排序 ---
|
||||
|
||||
def merge_sort(arr):
|
||||
"""
|
||||
实现归并排序算法
|
||||
参数arr: 待排序的交通数据数组
|
||||
返回: 一个新的、排序好的数组
|
||||
"""
|
||||
# TODO: 实现归并排序的递归逻辑
|
||||
# 提示:当数组元素个数小于等于1时,递归终止
|
||||
|
||||
|
||||
def merge(left, right):
|
||||
"""
|
||||
合并两个已排序的子数组
|
||||
参数left: 左侧已排序数组
|
||||
参数right: 右侧已排序数组
|
||||
返回: 合并后的一个有序数组
|
||||
"""
|
||||
|
||||
|
||||
#--- 测试与对比部分 ---
|
||||
|
||||
def measure_performance(sort_func, data):
|
||||
start_time = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6
|
||||
|
||||
network_sizes = [1000, 5000, 10000, 50000]
|
||||
print("算法性能对比测试:")
|
||||
for size in network_sizes:
|
||||
# 生成逆序数据,这是插入排序的最坏情况
|
||||
worst_case_data = list(range(size, 0, -1))
|
||||
|
||||
time_insertion = measure_performance(insertion_sort, worst_case_data)
|
||||
time_merge = measure_performance(merge_sort, worst_case_data)
|
||||
|
||||
print(f"--- 网络规模 n={size} (最坏情况) ---")
|
||||
print(f" - 插入排序: {time_insertion:.2f} ms")
|
||||
print(f" - 归并排序: {time_merge:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>分析与讨论</h5>
|
||||
<p>完成编程并得到输出后,请与右侧Agent讨论以下问题:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong>和<strong>空间成本</strong>来陈述你的理由。</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>进一步研究分治问题的时间复杂度分析</h3>
|
||||
<p>分治问题的两种时间复杂度分析方法:主方法和递归树法。</p>
|
||||
<h4>主方法</h4>
|
||||
<p>主方法本质是一种速算公式,针对于分治迭代式是这样的形式的:T(n)=aT(n/b)+f(n),a>=1,b>1。
|
||||
主方法概念上理解,就是比较分治法的“子问题总数”和“合并总数”,在问题复杂度上,哪个为“主”,就按照这个主部分计算时间复杂度。
|
||||
(因为时间复杂度公式上,我们只会保留增长最快的那一项,也就是“主项”)</p>
|
||||
<p>严格定义这里就略了,直接看口诀:</p>
|
||||
<ol>
|
||||
<li>算log_b (a),得到n^log_b (a);</li>
|
||||
<li>比f(n)和n^log_b (a)的增长速度:</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>前者慢 → O(n^log_b (a));</li>
|
||||
<li>差不多 → O(n^log_b (a) · log n)(k=0 时);</li>
|
||||
<li>前者快且满足正则条件 → O(f(n))。</li>
|
||||
<li>其中正则条件是存在0<c<1,使得:a·f(n/b)≤ c·n²</li>
|
||||
</ul>
|
||||
<p>试着计算一下:T(n) = 3T(n/2) + O(n²) 这个迭代式的时间复杂度</p>
|
||||
<h4>递归树法</h4>
|
||||
<p>递归树相对更容易理解(可能之前的问题你已经在不知觉中使用递归树法),因为这种方法完全就是对分治方法本身的全流程模拟。</p>
|
||||
<p>步骤 1:画树 —— 把问题拆解成 “节点”,标记每个节点的分解与合并耗时(父节点的解决耗时其实就是自己的所有子节点的分解与合并耗时)
|
||||
a. 根节点:写原问题的规模 n 和分解 / 合并耗时 f (n)
|
||||
b. 子节点:根节点拆成几个子问题,就画几个子节点,每个子节点写 “子问题规模 + 子问题的分解 / 合并耗时”
|
||||
c. 叶子节点:当子问题规模小到能直接解决(比如 n=1),就停止拆解,叶子节点写 “规模 1,耗时 Θ(1)”
|
||||
步骤 2:算层 —— 求每一层的总耗时
|
||||
同一层的节点,代表 “同一轮拆解的所有问题”,它们的耗时总和就是这一层的总代价
|
||||
步骤 3:求和 —— 总耗时 = 所有层的代价之和
|
||||
先确定树有多少层,再把每层的总代价加起来</p>
|
||||
<p>试着计算:无法用主方法解决的问题: T (n)=T (n/3)+T (2n/3)+2*n</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>算法设计范式:分而治之与归并排序</h1>
|
||||
<blockquote>
|
||||
<p>Auto-generated at 2025-09-22 15:38</p>
|
||||
</blockquote>
|
||||
<h2>算法设计范式:分而治之与归并排序</h2>
|
||||
<h3>分治法:分而治之</h3>
|
||||
<h4>引入</h4>
|
||||
<p>在算法分析与设计中,有一些设计算法解决问题的通用方法可供大家选取尝试,其中一个很常用的方法就叫做“分治法”,字如其意,
|
||||
分治法就是将大问题分解为小问题(或说子问题),并将子问题持续分解下去,直到子问题可以用常数时间进行求解。
|
||||
最基础的问题:何时可以用分治法?</p>
|
||||
<ol>
|
||||
<li>找最值问题</li>
|
||||
<li>回文字符串</li>
|
||||
</ol>
|
||||
<h4>分治法的组成</h4>
|
||||
<p>分治法有3个重要组成:分解、解决、合并。</p>
|
||||
<p>有一些帮助我们进行这些操作的分析与设计原则:</p>
|
||||
<ol>
|
||||
<li><strong>子问题相似</strong>:分解出的子问题与原问题类型一致,可复用相同解法,保证递归或分治逻辑的一致性。</li>
|
||||
<li><strong>子问题互不干扰</strong>:子问题边界清晰、数据独立,避免相互影响,简化问题拆解与求解逻辑。</li>
|
||||
<li><strong>合并代价可控</strong>:子问题解合并为原问题解的时间 / 空间开销在可接受范围,避免抵消分治带来的效率提升。</li>
|
||||
</ol>
|
||||
<h4>分治法的时间复杂度迭代公式</h4>
|
||||
<p>T(n)= 子问题数量 * T( 子问题规模 ) + 合并这些子问题的解的用时</p>
|
||||
<h3>归并算法理论分析</h3>
|
||||
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
|
||||
<p>在上周的工作中,你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错,但在应对大规模拥堵(逆序数据)时,其性能瓶颈非常明显。本周,你的项目经理(Agent)将向你介绍一种更高效的算法设计思想——“分而治之”,并要求你掌握其代表算法:归并排序。</p>
|
||||
<h4>任务:理解与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>归并排序(Merge Sort)是解决大规模排序问题的利器。在投入编码之前,你必须先从理论上理解它为何如此高效。</p>
|
||||
<h5>思考题</h5>
|
||||
<p>请与右侧的Agent(你的项目经理)对话,清晰地回答以下问题,以证明你已理解核心思想:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>分治思想</strong>:根据课程资料,请解释什么是“分而治之”(Divide-and-Conquer)设计范式?它包含哪三个基本步骤?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
|
||||
<ul>
|
||||
<li><code>2</code> 代表什么?</li>
|
||||
<li><code>T(n/2)</code> 代表什么?</li>
|
||||
<li><code>Θ(n)</code> 代表什么?</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>效率对比</strong>:你的同事认为,插入排序经过极致优化后,处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时(即n趋于无穷大时),哪个算法最终会胜出?请从渐进增长的角度解释你的理由。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
|
||||
<ul>
|
||||
<li><eq>n^2</eq> (插入排序最坏情况)</li>
|
||||
<li>nlogn (归并排序)</li>
|
||||
<li>n (一种非常低效的蛮力算法)</li>
|
||||
<li><eq>2^n</eq> (另一种指数级算法)</li>
|
||||
<li>logn (类似二分查找的效率)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>归并排序的编程实现</h3>
|
||||
<h4>任务:编码与对比</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
|
||||
<h5>题目:实现归并排序并对比性能</h5>
|
||||
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code> 和 <code>merge(left, right)</code> 两个函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
#上周实现的插入排序(用于对比)
|
||||
def insertion_sort(arr):
|
||||
for i in range(1, len(arr)):
|
||||
key = arr[i]
|
||||
j = i - 1
|
||||
while j >= 0 and arr[j] > key:
|
||||
arr[j + 1] = arr[j]
|
||||
j -= 1
|
||||
arr[j + 1] = key
|
||||
return arr
|
||||
|
||||
#--- 本周任务:请在下方实现归并排序 ---
|
||||
|
||||
def merge_sort(arr):
|
||||
"""
|
||||
实现归并排序算法
|
||||
参数arr: 待排序的交通数据数组
|
||||
返回: 一个新的、排序好的数组
|
||||
"""
|
||||
# TODO: 实现归并排序的递归逻辑
|
||||
# 提示:当数组元素个数小于等于1时,递归终止
|
||||
|
||||
|
||||
def merge(left, right):
|
||||
"""
|
||||
合并两个已排序的子数组
|
||||
参数left: 左侧已排序数组
|
||||
参数right: 右侧已排序数组
|
||||
返回: 合并后的一个有序数组
|
||||
"""
|
||||
|
||||
|
||||
#--- 测试与对比部分 ---
|
||||
|
||||
def measure_performance(sort_func, data):
|
||||
start_time = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6
|
||||
|
||||
network_sizes = [1000, 5000, 10000, 50000]
|
||||
print("算法性能对比测试:")
|
||||
for size in network_sizes:
|
||||
# 生成逆序数据,这是插入排序的最坏情况
|
||||
worst_case_data = list(range(size, 0, -1))
|
||||
|
||||
time_insertion = measure_performance(insertion_sort, worst_case_data)
|
||||
time_merge = measure_performance(merge_sort, worst_case_data)
|
||||
|
||||
print(f"--- 网络规模 n={size} (最坏情况) ---")
|
||||
print(f" - 插入排序: {time_insertion:.2f} ms")
|
||||
print(f" - 归并排序: {time_merge:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>分析与讨论</h5>
|
||||
<p>完成编程并得到输出后,请与右侧Agent讨论以下问题:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong>和<strong>空间成本</strong>来陈述你的理由。</p>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>进一步研究分治问题的时间复杂度分析</h3>
|
||||
<p>分治问题的两种时间复杂度分析方法:主方法和递归树法。</p>
|
||||
<h4>主方法</h4>
|
||||
<p>主方法本质是一种速算公式,针对于分治迭代式是这样的形式的:T(n)=aT(n/b)+f(n),a>=1,b>1。
|
||||
主方法概念上理解,就是比较分治法的“子问题总数”和“合并总数”,在问题复杂度上,哪个为“主”,就按照这个主部分计算时间复杂度。
|
||||
(因为时间复杂度公式上,我们只会保留增长最快的那一项,也就是“主项”)</p>
|
||||
<p>严格定义这里就略了,直接看口诀:</p>
|
||||
<ol>
|
||||
<li>算log_b (a),得到n^log_b (a);</li>
|
||||
<li>比f(n)和n^log_b (a)的增长速度:</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>前者慢 → O(n^log_b (a));</li>
|
||||
<li>差不多 → O(n^log_b (a) · log n)(k=0 时);</li>
|
||||
<li>前者快且满足正则条件 → O(f(n))。</li>
|
||||
<li>其中正则条件是存在0<c<1,使得:a·f(n/b)≤ c·n²</li>
|
||||
</ul>
|
||||
<p>试着计算一下:T(n) = 3T(n/2) + O(n²) 这个迭代式的时间复杂度</p>
|
||||
<h4>递归树法</h4>
|
||||
<p>递归树相对更容易理解(可能之前的问题你已经在不知觉中使用递归树法),因为这种方法完全就是对分治方法本身的全流程模拟。</p>
|
||||
<p>步骤 1:画树 —— 把问题拆解成 “节点”,标记每个节点的分解与合并耗时(父节点的解决耗时其实就是自己的所有子节点的分解与合并耗时)
|
||||
a. 根节点:写原问题的规模 n 和分解 / 合并耗时 f (n)
|
||||
b. 子节点:根节点拆成几个子问题,就画几个子节点,每个子节点写 “子问题规模 + 子问题的分解 / 合并耗时”
|
||||
c. 叶子节点:当子问题规模小到能直接解决(比如 n=1),就停止拆解,叶子节点写 “规模 1,耗时 Θ(1)”
|
||||
步骤 2:算层 —— 求每一层的总耗时
|
||||
同一层的节点,代表 “同一轮拆解的所有问题”,它们的耗时总和就是这一层的总代价
|
||||
步骤 3:求和 —— 总耗时 = 所有层的代价之和
|
||||
先确定树有多少层,再把每层的总代价加起来</p>
|
||||
<p>试着计算:无法用主方法解决的问题: T (n)=T (n/3)+T (2n/3)+2*n</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第二章:分治法-分治法实践与优化.html
Normal file
172
Html/apps/static/68bacdfadf5aeae0912f7f18-第二章:分治法-分治法实践与优化.html
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>分治策略进阶与主方法</h1>
|
||||
<blockquote>
|
||||
<p>Auto-generated at 2025-09-22 15:44</p>
|
||||
</blockquote>
|
||||
<h2>分治策略进阶与主方法</h2>
|
||||
<h3>最大子数组问题</h3>
|
||||
<h4>任务:编码与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
|
||||
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>,总增量最大的连续时段是从第8天到第11天,总增量为 <code>18 + 20 - 7 + 12 = 43</code> 。
|
||||
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
|
||||
<h5>题目:最大交通流量增量</h5>
|
||||
<p>你需要实现一个分治算法来解决最大子数组问题。
|
||||
同时,为了对比,你也会实现一个暴力求解算法。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
|
||||
<pre><code class="language-python">import time
|
||||
import random
|
||||
|
||||
def find_maximum_subarray_brute(arr):
|
||||
"""
|
||||
使用暴力法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
|
||||
#--- 本周任务:请在下方实现分治算法 ---
|
||||
|
||||
def find_max_crossing_subarray(arr, low, mid, high):
|
||||
"""
|
||||
找到跨越中点的最大子数组
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现寻找跨越中点的最大子数组的逻辑
|
||||
# 提示: 从mid向左和向右分别扫描,找到各自的最大和
|
||||
|
||||
def find_maximum_subarray(arr, low, high):
|
||||
"""
|
||||
使用分治法求解最大子数组问题
|
||||
返回: (最大和, 开始索引, 结束索引)
|
||||
"""
|
||||
# TODO: 实现分治递归逻辑
|
||||
# 提示: 递归基是当数组只有一个元素时
|
||||
|
||||
#--- 测试与对比部分 --- 以下内容无需修改
|
||||
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
|
||||
print(f"交通流量变化数据: {traffic_changes}")
|
||||
|
||||
#使用分治法
|
||||
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
|
||||
print(f"分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}")
|
||||
|
||||
#使用暴力法验证
|
||||
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
|
||||
print(f"暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}")
|
||||
|
||||
#性能测试
|
||||
large_data = [random.randint(-50, 50) for _ in range(2000)]
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray(large_data, 0, len(large_data) - 1)
|
||||
dc_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms")
|
||||
|
||||
start_time = time.perf_counter()
|
||||
find_maximum_subarray_brute(large_data)
|
||||
brute_time = (time.perf_counter() - start_time) * 1000
|
||||
print(f"在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms")
|
||||
</code></pre>
|
||||
<h3>分治经典算法:快速排序</h3>
|
||||
<h4>快速排序原理</h4>
|
||||
<p>快速排序(Quick Sort)是分治法的经典应用之一。与归并排序不同的是,快速排序通过原地划分数组来完成排序,无需额外的同规模辅助存储空间。它在平均情况下表现极为高效,是实际应用中常用的排序算法之一。</p>
|
||||
<p>快速排序的分治核心思路非常好理解,对于一个未排序的数组,希望分解为左区间和一个“枢轴(pivot)”元素和右区间,其中左区间的数都小于等于枢轴,右区间的都大于等于枢轴。
|
||||
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
|
||||
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
|
||||
<h4>任务:编码与实验</h4>
|
||||
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
|
||||
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
|
||||
代码框架
|
||||
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
实现快速排序算法(固定枢轴策略)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
# TODO: 实现快速排序的分治逻辑
|
||||
# 提示:选取第一个元素为枢轴,递归排序左右子数组
|
||||
|
||||
def measure_performance(sort_func, data):
|
||||
start_time = time.perf_counter_ns()
|
||||
sort_func(data.copy()) # 对数据副本排序,避免影响原数据
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6 # 转换为毫秒
|
||||
|
||||
#性能测试:对比有序输入和随机输入
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("快速排序性能测试(固定枢轴):")
|
||||
for n in sizes:
|
||||
sorted_data = list(range(n))
|
||||
random_data = [random.randint(0, 10**6) for _ in range(n)]
|
||||
time_sorted = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(quick_sort, random_data)
|
||||
print(f"数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms")
|
||||
</code></pre>
|
||||
<p>完成编码并运行测试,报告时间差异。</p>
|
||||
<h3>快速排序的复杂度分析</h3>
|
||||
<h4>时间复杂度</h4>
|
||||
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong>和</strong>递归深度</strong>共同决定
|
||||
三种典型场景:最坏情况、平均情况、最佳情况</p>
|
||||
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
|
||||
平均情况:输入随机 / 等概率选枢轴
|
||||
最佳情况:每次划分平分数组</p>
|
||||
<h4>空间复杂度</h4>
|
||||
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
|
||||
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
|
||||
<h4>分析</h4>
|
||||
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
|
||||
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
|
||||
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
|
||||
<h4>实践</h4>
|
||||
<pre><code>import random
|
||||
import time
|
||||
|
||||
def random_quick_sort(arr):
|
||||
"""
|
||||
实现随机快速排序算法(随机选择枢轴)
|
||||
参数arr: 待排序的数组
|
||||
返回: 排序后的数组
|
||||
"""
|
||||
|
||||
|
||||
def quick_sort(arr):
|
||||
"""
|
||||
你在上一章实现的快排内容
|
||||
"""
|
||||
|
||||
#验证随机快排在极端有序输入下的性能
|
||||
n = 10000
|
||||
sorted_data = list(range(n))
|
||||
time_fixed = measure_performance(quick_sort, sorted_data)
|
||||
time_random = measure_performance(random_quick_sort, sorted_data)
|
||||
print(f"数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms")
|
||||
|
||||
</code></pre>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>动态规划例题训练</h1>
|
||||
<h2>动态规划例题训练</h2>
|
||||
<h3>步骤A</h3>
|
||||
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
|
||||
<h3>步骤B</h3>
|
||||
<p>继续补充该阶段的其它步骤内容。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
240
Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
Normal file
240
Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>动态规划原理</h1>
|
||||
<h2>动态规划原理</h2>
|
||||
<h3>自顶而下的分治 vs. 自底向上的动态规划</h3>
|
||||
<p>分治法:面对规模为 <em>n</em> 的问题,从<strong>顶层</strong>出发,将其拆为若干个更小的子问题,分别求解后<strong>合并</strong>。</p>
|
||||
<p>动态规划(DP):当<strong>同一子问题重复出现</strong>时,与其一再从顶层“把问题拆碎”,不如记录子问题的答案,并按规模<strong>从小到大</strong>把所有需要的子问题一次性求出来。
|
||||
<br></p>
|
||||
<h4>什么才算“同一”子问题</h4>
|
||||
<p>与AI教师讨论,如何区分“相同规模的子问题”和“同一(可复用的)子问题”</p>
|
||||
<h3>切绳(Rod Cutting)问题</h3>
|
||||
<p>给定一根长度为 <em>n</em> 的绳子,价格表 <code>price[i]</code> 表示长度为 <em>i</em> 的一段可以卖出的价格。允许将绳子切成多段出售,目标是使总收益最大。</p>
|
||||
<h4>分治法与时间复杂度</h4>
|
||||
<p>设 <code>R(n)</code> 为长度 <em>n</em> 的最大收益:</p>
|
||||
<section>
|
||||
<eqn> R(0) = 0, R(1)=price[1] </eqn>
|
||||
</section>
|
||||
<section>
|
||||
<eqn> R(n) = max_{1 ≤ i ≤ n} ( price[i] + R(n - i) ) </eqn>
|
||||
</section>
|
||||
<p><strong>纯递归分治</strong>会对同一规模的子问题多次求解,子问题规模组合数呈指数级,时间复杂度为 <strong>O(n^n)</strong> 量级。</p>
|
||||
<h4>记忆化(自顶向下)</h4>
|
||||
<p>思想:用哈希表/数组 <code>memo[n]</code> 记录 <code>R(n)</code>。当再次需要 <code>R(n)</code> 时,直接返回已存结果,避免重复计算。</p>
|
||||
<ul>
|
||||
<li><strong>复杂度</strong>:时间 <strong>O(n^2)</strong>(外层 n,内层枚举切第一刀 i),空间 <strong>O(n)</strong>。</li>
|
||||
</ul>
|
||||
<p><strong>代码任务 A:实现记忆化递归(Top-Down)</strong></p>
|
||||
<pre><code class="language-python">def rod_cut_topdown(price: dict[int, int], n: int, memo: list[int]) -> int:
|
||||
"""
|
||||
返回长度 n 的最大收益(记忆化递归)。
|
||||
TODO:
|
||||
1) 处理 n==0;2) 命中 memo 直接返回;3) 枚举第一刀长度 i;4) 写回 memo[n]。
|
||||
"""
|
||||
pass
|
||||
</code></pre>
|
||||
<h4>自底向上(反向记忆化)</h4>
|
||||
<p>将规模从小到大推进:<code>dp[x]</code> 表示长度 <code>x</code> 的最优收益。
|
||||
<strong>代码任务 B:实现自底向上(Bottom-Up)</strong></p>
|
||||
<pre><code class="language-python">def rod_cut_bottomup(price: dict[int, int], n: int) -> int:
|
||||
"""
|
||||
返回长度 n 的最大收益(自底向上)。
|
||||
TODO:
|
||||
1) 初始化 dp[0]=0;2) for x in 1..n:dp[x] = max_{1..x}( price[i] + dp[x-i] )
|
||||
"""
|
||||
|
||||
"""
|
||||
以下内容无需修改,注意将你实现的rod_cut_topdown代码复制过来
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import signal
|
||||
from functools import wraps
|
||||
|
||||
|
||||
#超时异常定义
|
||||
class TimeoutError(Exception):
|
||||
pass
|
||||
|
||||
#超时装饰器
|
||||
def timeout(seconds):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 定义超时处理函数
|
||||
def handle_timeout(signum, frame):
|
||||
raise TimeoutError(f"Function {func.__name__} timed out after {seconds} seconds")
|
||||
|
||||
# 设置信号处理
|
||||
signal.signal(signal.SIGALRM, handle_timeout)
|
||||
signal.alarm(seconds) # 触发超时
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
finally:
|
||||
signal.alarm(0) # 取消超时
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
#带超时的纯递归实现(用于对比)
|
||||
@timeout(1) # 1秒超时
|
||||
def rod_cut_recursive(price: dict[int, int], n: int) -> int:
|
||||
if n == 0:
|
||||
return 0
|
||||
max_rev = -float('inf')
|
||||
for i in range(1, n + 1):
|
||||
if i in price:
|
||||
max_rev = max(max_rev, price[i] + rod_cut_recursive(price, n - i))
|
||||
return max_rev
|
||||
|
||||
|
||||
#对比实验
|
||||
def compare_algorithms():
|
||||
# 生成测试用的价格表(随机生成1到10的价格)
|
||||
max_length = 50
|
||||
price = {i: random.randint(1, 10) for i in range(1, max_length + 1)}
|
||||
|
||||
print(f"{'n':<5} {'递归(ms)':<10} {'记忆化(ms)':<12} {'自底向上(ms)':<15}")
|
||||
print("-" * 50)
|
||||
|
||||
# 测试n从10到50的情况
|
||||
for n in range(10, 51, 5):
|
||||
# 纯递归(带超时处理)
|
||||
try:
|
||||
start = time.time()
|
||||
recursive_result = rod_cut_recursive(price, n)
|
||||
recursive_time = (time.time() - start) * 1000
|
||||
except TimeoutError:
|
||||
recursive_result = "超时"
|
||||
recursive_time = ">1000"
|
||||
|
||||
# 记忆化递归
|
||||
memo = [-1] * (n + 1)
|
||||
start = time.time()
|
||||
topdown_result = rod_cut_topdown(price, n, memo)
|
||||
topdown_time = (time.time() - start) * 1000
|
||||
|
||||
# 自底向上
|
||||
start = time.time()
|
||||
bottomup_result = rod_cut_bottomup(price, n)
|
||||
bottomup_time = (time.time() - start) * 1000
|
||||
|
||||
# 验证结果一致性(仅当递归未超时)
|
||||
if isinstance(recursive_result, int):
|
||||
assert recursive_result == topdown_result == bottomup_result, f"结果不一致 for n={n}"
|
||||
|
||||
# 输出结果
|
||||
print(f"{n:<5} {recursive_time:<10} {topdown_time:<12.4f} {bottomup_time:<15.4f}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
compare_algorithms()
|
||||
</code></pre>
|
||||
<p>完成上面的代码,讨论实验对比结果。</p>
|
||||
<h3>动态规划的“组成”:如何写出一个 DP</h3>
|
||||
<h4><strong>四大组成</strong></h4>
|
||||
<ul>
|
||||
<li><strong>状态空间</strong>:用最少的下标(或维度)刻画子问题(如 <code>dp[i]</code>、<code>dp[i][j]</code>)。</li>
|
||||
<li><strong>状态转移</strong>:写出“从更小状态到当前状态”的递推/转移式。</li>
|
||||
<li><strong>边界条件</strong>:初始已知的最小规模答案(如 <code>dp[0]=0</code>)。</li>
|
||||
<li><strong>解的恢复(部分题目可能不需要)</strong>:若需输出方案/路径,记录子问题选择来源(从那个子问题的答案转移而来)(如 <code>choice[i][j]</code>)。</li>
|
||||
</ul>
|
||||
<h4><strong>两大性质</strong>:</h4>
|
||||
<ul>
|
||||
<li><strong>最优子结构</strong>:全局最优由若干子问题的最优解组合而成;</li>
|
||||
<li><strong>重复子问题</strong>:不同路径会遇到同一个(或等价的)子问题。</li>
|
||||
</ul>
|
||||
<h3>例题:矩阵连乘(Matrix-Chain Multiplication, MCM)</h3>
|
||||
<h4>问题描述</h4>
|
||||
<p>矩阵乘法有严格的维度匹配要求:只有前一个矩阵的列数 = 后一个矩阵的行数,才能相乘。
|
||||
即:
|
||||
矩阵 A:维度为 m × k(共 m 行、k 列)
|
||||
矩阵 B:维度为 k × n(共 k 行、n 列)
|
||||
矩阵 C = A×B,维度为 m × n
|
||||
产生标量乘法次数为 <code>m×n×k</code>
|
||||
给定矩阵链 <code>A₁A₂…Aₖ</code>,维度数组 <code>p[0..k]</code> 满足 <code>Aᵢ</code> 大小为 <code>p[i-1] × p[i]</code>。目标:只改变乘法<strong>括号化顺序</strong>,最小化标量乘法次数。</p>
|
||||
<h4>递推与 DP 表</h4>
|
||||
<p>令 <code>m[i][j]</code> 表示从 <code>Aᵢ…Aⱼ</code> 的最小乘法次数(1-index)。则:</p>
|
||||
<section>
|
||||
<eqn> m[i][i] = 0 </eqn>
|
||||
</section>
|
||||
<section>
|
||||
<eqn> m[i][j] = min_{i ≤ k < j} ( 尝试推导一下这里的转移式 ) </eqn>
|
||||
</section>
|
||||
<h4>记录断点</h4>
|
||||
<p>令 <code>s[i][j]</code> 存储从 <code>Aᵢ…Aⱼ</code> 的最优断点。
|
||||
<eq>$ s[i][j] = k \text{ if } m[i][j] == 与上面的式子一样 $</eq></p>
|
||||
<h4>代码任务</h4>
|
||||
<pre><code class="language-python">def matrix_chain_order(p: list[int]) -> tuple[list[list[int]], list[list[int]]]:
|
||||
"""
|
||||
返回 (m, s),m[i][j] 为最小代价,s[i][j] 为最优断点。
|
||||
TODO: 长度 n = len(p)-1;按区间长度 L=2..n 填表。
|
||||
"""
|
||||
...
|
||||
|
||||
def print_optimal_parens(s: list[list[int]], i: int, j: int) -> str:
|
||||
"""根据断点矩阵 s 输出最优括号化方案"""
|
||||
if i == j:
|
||||
return f"A{i}"
|
||||
else:
|
||||
return f"({print_optimal_parens(s, i, s[i][j])}" \
|
||||
f"{print_optimal_parens(s, s[i][j]+1, j)})"
|
||||
|
||||
#测试验证部分
|
||||
if __name__ == "__main__":
|
||||
# 经典测试案例
|
||||
p = [30, 35, 15, 5, 10, 20, 25]
|
||||
expected_result = 15125
|
||||
|
||||
# 计算最优解
|
||||
m, s = matrix_chain_order(p)
|
||||
n = len(p) - 1
|
||||
result = m[1][n]
|
||||
|
||||
# 输出测试结果
|
||||
print(f"矩阵维度数组: {p}")
|
||||
print(f"矩阵数量: {n}")
|
||||
print(f"计算得到的最小标量乘法次数: {result}")
|
||||
print(f"预期的最小标量乘法次数: {expected_result}")
|
||||
print(f"测试{'通过' if result == expected_result else '失败'}")
|
||||
print(f"最优括号化方案: {print_optimal_parens(s, 1, n)}")
|
||||
|
||||
# 额外测试案例
|
||||
p2 = [40, 20, 30, 10, 30]
|
||||
m2, s2 = matrix_chain_order(p2)
|
||||
print("\n第二个测试案例:")
|
||||
print(f"矩阵维度数组: {p2}")
|
||||
print(f"最小标量乘法次数: {m2[1][4]}") # 预期结果为 26000
|
||||
print(f"最优括号化方案: {print_optimal_parens(s2, 1, 4)}")
|
||||
|
||||
</code></pre>
|
||||
<h4>综合讨论</h4>
|
||||
<ul>
|
||||
<li>何时选“记忆化 Top-Down”,何时选“自底向上表格法”?</li>
|
||||
<li>如何从“纯递归”快速判断是否值得改造成 DP?</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第四周-基于比较的排序.html
Normal file
159
Html/apps/static/68bacdfadf5aeae0912f7f18-第四周-基于比较的排序.html
Normal file
@@ -0,0 +1,159 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>基于比较的排序</h1>
|
||||
<h2>基于比较的排序</h2>
|
||||
<h3>优先队列的原理</h3>
|
||||
<p><strong>优先队列(Priority Queue)</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
|
||||
<br>
|
||||
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
|
||||
<br><br>
|
||||
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
|
||||
<br><br>
|
||||
<strong>堆(Heap)结构</strong>是实现优先队列的首选。</p>
|
||||
<h3>数组模拟堆实现的优先队列</h3>
|
||||
<h4>堆结构与取存原理</h4>
|
||||
<p>堆(Heap)本质上是一个完全二叉树结构:
|
||||
(当然也可以是多叉树,但没有必要)
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1:大根堆的二叉树结构图示" /></p>
|
||||
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
|
||||
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
|
||||
<h5>取数</h5>
|
||||
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
|
||||
但是将这个数取出后还不能结束,因为需要维护堆的性质。
|
||||
<br>
|
||||
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
|
||||
<br></p>
|
||||
<h5>存数</h5>
|
||||
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
|
||||
<h4>数组模拟堆</h4>
|
||||
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2:左边是堆的数组表示,右边是其对应的二叉树" />
|
||||
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
|
||||
<p>数组下标从1开始,任意一个下标i,其左右儿子下标刚好就是"i*2"和“i*2+1”。这样在后续代码实现时,代码写起来就会简单很多。</p>
|
||||
<h6>建堆</h6>
|
||||
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
|
||||
对于一个元素随机的数组,只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
|
||||
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
|
||||
<h3>优先队列的算法实现</h3>
|
||||
<h4>练习:堆操作</h4>
|
||||
<p>在进行代码实现之前,做一个问题练习:
|
||||
对于一个随机队列"[3, 32, 6, 43, 5, 8, 0, 9]",经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
|
||||
将答案告知AI教师。</p>
|
||||
<h4>任务:城市事件优先调度</h4>
|
||||
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
|
||||
<p><br><br></p>
|
||||
<h4>题目:实现堆排序</h4>
|
||||
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
|
||||
<br>
|
||||
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
|
||||
<br>
|
||||
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
|
||||
<br></p>
|
||||
<h5>代码框架</h5>
|
||||
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
def max_heapify(arr, n, i):
|
||||
"""
|
||||
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
|
||||
调整结点 i 使以其为根的子树成为最大堆
|
||||
参数:
|
||||
arr: 存储堆的数组
|
||||
n: 堆的有效大小(长度)
|
||||
i: 需要下滤调整的节点索引
|
||||
"""
|
||||
# TODO: 在此处实现 "下滤" 操作,将 arr[i] 下沉到正确位置
|
||||
|
||||
|
||||
def build_max_heap(arr):
|
||||
"""
|
||||
将无序数组原地建成最大堆,从后往前进行下滤
|
||||
参数:
|
||||
arr: 待调整的数组
|
||||
"""
|
||||
# TODO: 调用 max_heapify 将 arr 调整为堆
|
||||
|
||||
|
||||
def heap_sort(arr):
|
||||
"""
|
||||
利用堆排序算法排序数组(降序)
|
||||
参数:
|
||||
arr: 待排序数组
|
||||
返回:
|
||||
排序后的数组(从大到小)
|
||||
"""
|
||||
# TODO: 完成堆排序的实现
|
||||
n = len(arr)
|
||||
# 1. 原地建堆
|
||||
build_max_heap(arr)
|
||||
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
|
||||
|
||||
|
||||
#性能测试:对比不同规模输入的堆排序用时
|
||||
def measure(sort_func, data):
|
||||
start = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end = time.perf_counter_ns()
|
||||
return (end - start) / 10**6 # 毫秒
|
||||
|
||||
sizes = [1000, 5000, 10000]
|
||||
print("堆排序性能测试:")
|
||||
for n in sizes:
|
||||
data = [random.randint(0, 1000000) for _ in range(n)]
|
||||
t = measure(heap_sort, data)
|
||||
print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>实验结果分析</h5>
|
||||
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>,当输入规模增大时,运行时间应呈现近似线性乘以对数的增长趋势。具体来说,若将输入规模扩大10倍,运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
|
||||
<br>
|
||||
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比,你会发现堆排序对规模扩大的响应增长显著缓慢得多。
|
||||
<br>
|
||||
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
|
||||
<br>
|
||||
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
|
||||
<h3>比较排序的决策树模型</h3>
|
||||
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
|
||||
<br>
|
||||
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>。
|
||||
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
|
||||
<p>决策树是描述比较排序过程的一种抽象模型。
|
||||
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
|
||||
<br>
|
||||
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
|
||||
<br>
|
||||
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
|
||||
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
|
||||
<br>
|
||||
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
|
||||
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
|
||||
<br>
|
||||
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
|
||||
<br>
|
||||
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
|
||||
<br>
|
||||
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>,因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符:对三个无任何特殊性质的数进行排序,最少需要3次比较才能确定它们的正确顺序。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
112
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-二分查找.html
Normal file
112
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-二分查找.html
Normal file
@@ -0,0 +1,112 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>二分查找</h1>
|
||||
<h2>二分查找</h2>
|
||||
<h3>二分查找的原理</h3>
|
||||
<p>二分查找是一个基础但很重要的知识点,也是一种特殊的分治,为以后许多高级的数据结构与算法铺垫。</p>
|
||||
<p>下面是一个用二分的简单场景:</p>
|
||||
<p>假设小明从0到1000之间选择了一个数字但不告诉你,你可以不断猜测这个数,每次猜测小明会告知你的猜测得过大还是过小,问最多几次就一定能猜中?</p>
|
||||
<p>答案是利用二分查找的原理,猜测11次即可。</p>
|
||||
<ol>
|
||||
<li>对于0到1000的答案备选区,猜测中位数500,假设过小,</li>
|
||||
<li>则对于501到1000的答案备选区,猜测750,假设过大</li>
|
||||
<li>则对于501到749的答案备选区,猜测625,假设过小,</li>
|
||||
<li>则对于626到749区间......</li>
|
||||
<li>(688-749)</li>
|
||||
<li>(718-749)</li>
|
||||
<li>(734-749)</li>
|
||||
<li>(742-749)</li>
|
||||
<li>(746-749)</li>
|
||||
<li>(748-749)</li>
|
||||
<li>(749-749)</li>
|
||||
</ol>
|
||||
<p>在最差的情况下,第11次的答案备选区就一定长度为1了,也就是必然是答案。</p>
|
||||
<p>因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。</p>
|
||||
<h4>例题</h4>
|
||||
<p>对于上面那个题目,如果问题区间是1到4000,最差情况下需要猜测几次?这个值可以怎么迅速地算出来,你可以用时间复杂度的公式建立一下并推导一下么?</p>
|
||||
<h3>练习:二分查找</h3>
|
||||
<p>试试对于下面的题目,用代码实现一下二分查找。</p>
|
||||
<h4>题目:有序数组寻址</h4>
|
||||
<p>给出一个长度为n的有序数组(从小到大),有q次询问,对于每次询问,输出指定数在数组中的下标。如果不存在则输出-1。</p>
|
||||
<h5>输入</h5>
|
||||
<p>第一行一个整数n。(1<=n<=10^5)</p>
|
||||
<p>第二行n个用空格分开的整数ai。(0<=ai<=10^8)</p>
|
||||
<p>第三行一个整数q,表示询问的次数。(1<=q<=10^4)</p>
|
||||
<p>后q行,每行一个整数b,表示询问的数。(0<=b<=10^8)</p>
|
||||
<h5>输出</h5>
|
||||
<p>q行,每行一个整数,对应每次询问的返回结果</p>
|
||||
<h5>提示:</h5>
|
||||
<p>完成代码后,通知Agent进行评测。</p>
|
||||
<p>如果你还不完全会这个算法,询问Agent获取提示并进行学习。</p>
|
||||
<h3>二维二分查找</h3>
|
||||
<h4>二分查找的直觉</h4>
|
||||
<p>通过之前的原理和实现,二分查找本质上是通过取中的方式,尽可能排除多(一半)的备选数。
|
||||
<br>
|
||||
为进一步理解,除了序列,本节我们来尝试一下在矩阵(二维数组)上进行分治和查找。</p>
|
||||
<h4>问题建模</h4>
|
||||
<p>给出一个n*n的矩阵,其中每一行都是一个从小到大的序列,每一列都是从小到大的序列。从中找到指定的一个数target。</p>
|
||||
<h5>例</h5>
|
||||
<p>[
|
||||
[1, 2, 4, 5]
|
||||
[2, 5, 7, 11]
|
||||
[3, 8, 10, 12]
|
||||
[4, 10, 17, 20]
|
||||
]
|
||||
从中找到"8"这个数。</p>
|
||||
<h5>线性做法</h5>
|
||||
<p>这里先介绍线性做法。
|
||||
一维序列的线性做法就是逐个比对一下,
|
||||
二维做法最差是逐个扫描n*n所有的数,可以聪明一些降低到线性成本,称为“楼梯式”查找:</p>
|
||||
<ol>
|
||||
<li>从右上角看一个元素 x = M[r][c]</li>
|
||||
<li>查找与排除
|
||||
a. 若 x > target,则这一列里 x 下方都 ≥ x,更不可能是 target,所以安全地左移(排除一整列)。
|
||||
b. 若 x < target,则这一行里 x 左边都 ≤ x,更不可能是 target,所以安全地下移(排除一整行)。</li>
|
||||
<li>持续直到排除所有行和列,从而找到目标元素或告知找不到。</li>
|
||||
</ol>
|
||||
<p>做法正确性分析,时间复杂度分析。</p>
|
||||
<h4>分治做法:二维二分</h4>
|
||||
<p>接下来试着通过二分的技术,找找复杂度更低的做法。
|
||||
试着回答这些问题,并与AI教师进行讨论:</p>
|
||||
<ol>
|
||||
<li>分解中点在哪里?</li>
|
||||
<li>二分后排除掉的部分是哪些?</li>
|
||||
<li>如何划分子问题?</li>
|
||||
<li>时间复杂度是多少?</li>
|
||||
</ol>
|
||||
<h3>二维二分查找实现</h3>
|
||||
<h4>题目:有序矩阵查找</h4>
|
||||
<p>给出一个 n×n 的矩阵,其中每一行的元素都按照从小到大的顺序排列,每一列的元素也都按照从小到大的顺序排列。现需要判断指定的数 target 是否在该矩阵中,若存在则输出其所在的行下标和列下标(行和列均从 0 开始计数);若不存在则输出 - 1 -1。</p>
|
||||
<h4>输入</h4>
|
||||
<p>第一行一个整数 n。(1<=n<=10^3)
|
||||
接下来 n 行,每行 n 个用空格分开的整数,表示矩阵的元素。
|
||||
最后一行一个整数 target,表示需要查找的数。</p>
|
||||
<h4>输出</h4>
|
||||
<p>一行两个整数,分别表示 target 所在的行下标和列下标,中间用空格隔开。若不存在则输出 - 1 -1。</p>
|
||||
<h4>示例</h4>
|
||||
<p>输入:41 2 4 52 5 7 113 8 10 124 10 17 208
|
||||
输出:2 1</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
198
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-分位统计量.html
Normal file
198
Html/apps/static/68bacdfadf5aeae0912f7f18-第四章:查询-分位统计量.html
Normal file
@@ -0,0 +1,198 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>分位统计量</h1>
|
||||
<h2>分位统计量</h2>
|
||||
<h3>第K大的数</h3>
|
||||
<p>在许多算法与数据分析任务中,我们不仅需要排序整个数组,还需要找出某个特定顺序统计量,例如:
|
||||
• 一个数组中第 K 大的元素;
|
||||
• 所有元素的中位数(K = n/2);
|
||||
• 某个分位点(25%、75%等)…
|
||||
<br>
|
||||
最直接的办法是先对数组进行排序,然后取第 K 个位置的元素。这种方法的时间复杂度为 <eq>O(n \log n)</eq>,因为我们需要对所有元素进行排序。
|
||||
<br>
|
||||
但我们真正关心的,只是一个位置的元素 —— 我们是否有更快的方法?
|
||||
<br></p>
|
||||
<h4>快速选择(QuickSelect)</h4>
|
||||
<p><strong>快排中的“轴枢”启发</strong>
|
||||
我们在快速排序中,每次选一个枢轴 pivot,通过划分操作把数组分成两部分:
|
||||
• 左边元素 <= pivot;
|
||||
• 右边元素 >= pivot。
|
||||
最终 pivot 会被放置固定位置上。
|
||||
<br>
|
||||
可见:</p>
|
||||
<blockquote>
|
||||
<p>若 pivot 恰好是我们要找的第 K 大元素,就不必继续递归。</p>
|
||||
</blockquote>
|
||||
<br>
|
||||
因此可得快速选择的具体做法,先令target等于N-K,即找第N-K+1小的数,其下标为target:
|
||||
1. 随机选一个枢轴;
|
||||
2. 利用快速排序的 partition 划分函数,找出 pivot 的位置 idx;
|
||||
3. 比较该位置与目标索引:
|
||||
• 若 idx == target,返回 pivot;
|
||||
• 若 idx > target,在左半边递归查找;
|
||||
• 否则在右半边递归查找。
|
||||
<br>
|
||||
理解做法并分析时间复杂度。
|
||||
### 快速选择的实现
|
||||
<p>实现一个 <code>quickselect(arr, k)</code> 函数,返回第 K 大的元素
|
||||
(也就是增序列下标N-K位置的数)</p>
|
||||
<pre><code class="language-python">import random
|
||||
|
||||
def partition(arr, low, high):
|
||||
"""
|
||||
对arr序列从low到high下标,找到一个枢轴,并返回其下标。(在arr中,比枢轴小的数都在idx左边,大的在右边)
|
||||
"""
|
||||
|
||||
def quickselect(arr, k):
|
||||
"""
|
||||
快速选择第K大元素(转换为第n-k小)
|
||||
:param arr: 输入数组
|
||||
:param k: 要找的第K大元素
|
||||
:return: 该元素值
|
||||
"""
|
||||
n = len(arr)
|
||||
target = n - k # 第k大转为第n-k小(序列下标)
|
||||
low, high = 0, n - 1
|
||||
|
||||
while low <= high:
|
||||
idx = partition(arr, low, high)
|
||||
if idx == target:
|
||||
return arr[idx]
|
||||
elif idx < target:
|
||||
low = idx + 1
|
||||
else:
|
||||
high = idx - 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
#测试1:随机数组
|
||||
random.seed(42) # 设置随机种子,确保结果可重现
|
||||
arr1 = [random.randint(1, 100) for _ in range(10)]
|
||||
k1 = 3
|
||||
result1 = quickselect(arr1.copy(), k1) # 使用副本避免修改原数组
|
||||
sorted_arr1 = sorted(arr1)
|
||||
expected1 = sorted_arr1[-k1]
|
||||
print(f"测试1 - 随机数组:")
|
||||
print(f"原数组: {arr1}")
|
||||
print(f"第{k1}大元素: {result1}, 预期值: {expected1}")
|
||||
print(f"测试{'通过' if result1 == expected1 else '失败'}\n")
|
||||
|
||||
#测试2:包含重复元素的数组
|
||||
arr2 = [5, 3, 8, 8, 1, 5, 9, 3, 5]
|
||||
k2 = 2
|
||||
result2 = quickselect(arr2.copy(), k2)
|
||||
sorted_arr2 = sorted(arr2)
|
||||
expected2 = sorted_arr2[-k2]
|
||||
print(f"测试2 - 包含重复元素:")
|
||||
print(f"原数组: {arr2}")
|
||||
print(f"第{k2}大元素: {result2}, 预期值: {expected2}")
|
||||
print(f"测试{'通过' if result2 == expected2 else '失败'}\n")
|
||||
|
||||
#测试3:边界条件 - k=1(最大元素)
|
||||
arr3 = [10, 20, 30, 40, 50]
|
||||
k3 = 1
|
||||
result3 = quickselect(arr3.copy(), k3)
|
||||
sorted_arr3 = sorted(arr3)
|
||||
expected3 = sorted_arr3[-k3]
|
||||
print(f"测试3 - 最大元素:")
|
||||
print(f"原数组: {arr3}")
|
||||
print(f"第{k3}大元素: {result3}, 预期值: {expected3}")
|
||||
print(f"测试{'通过' if result3 == expected3 else '失败'}\n")
|
||||
</code></pre>
|
||||
<h3>BFPRT 算法:中位数的中位数算法</h3>
|
||||
<p>快速选择的平均复杂度为 <eq>O(n)</eq>,但可能退化为 <eq>O(n^2)</eq>。</p>
|
||||
<h4>BFPRT分治法</h4>
|
||||
<p>BFPRT算法和二分查找中对“矩阵”进行分治的做法很像,也是通过矩阵的中心数,来排除一部分数据。
|
||||
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" />
|
||||
<br>
|
||||
我们通过以下几个步骤确保选出的“轴枢”可以高概率地“削减”足够多的元素,从而保证递归过程每次至少减少 30% 左右的输入规模:
|
||||
<br></p>
|
||||
<ol>
|
||||
<li>将原始数组划分为若干个 5 个元素的组
|
||||
请看图中所有的竖列(粉红色背景小矩形):每个小矩形内有 5 个点,这就是我们将原始数据分成的小组。
|
||||
组的个数 ≈ n / 5(设 n 是总元素数量)
|
||||
不足 5 个的组,可以用一个全局最小值(如 -1)补全。
|
||||
<br></li>
|
||||
<li>对每组内部排序,取出中位数
|
||||
图中每个竖列的小组中,绿色圆点表示每组的元素。中间处于红框中的数为每组中位数。
|
||||
每列上两个数小于中位数,下两个大于。
|
||||
<br></li>
|
||||
<li>将所有组的中位数组成新数组,对其递归调用本算法找中位数。
|
||||
图中红色矩形框住的内容,正是“所有组中位数”的集合。这些点来自所有小组的中位点,组成新的长度为 n/5 的数组。
|
||||
对这个新数组再次使用本算法,递归找出它的中位数,作为我们最终要用的“枢轴”。即图中棕色点。
|
||||
<br></li>
|
||||
<li>这个中位数(棕色圆点)就是我们分治的“枢轴”
|
||||
有如下重要性质:
|
||||
a. 棕点及左上角(图中蓝框的点)均小于等于枢轴
|
||||
b. 棕点及右下角(图中黑框的点)均大于等于枢轴
|
||||
<br></li>
|
||||
<li>进行排除,分解,先令x=n-k+1;找第k大数就是找第x小数
|
||||
a. 若x>蓝框数数量,则在非蓝框数中找第k大
|
||||
b. 若k>黑框数数量,则在非黑框数中找第(k-黑框数数量)大
|
||||
c. 当k=1或n'(n'是当前子问题的数数量),或n'<=5,均可在不大于O(n')的用时完成找数。
|
||||
可以证明在c不成立的情况下,a,b两点至少成立一个。
|
||||
<br></li>
|
||||
</ol>
|
||||
<p>与AI教师讨论每一步在做的事和最终想要实现的效果。可以联想你之前学习到的二分查找中的矩阵分治。</p>
|
||||
<h3>BFPRT 复杂度分析</h3>
|
||||
<p>在理解了“中位数的中位数”算法的流程之后,我们接下来要正式分析它的<strong>时间复杂度</strong>,并证明该算法的整体运行时间为 <eq>O(n)</eq>。</p>
|
||||
<hr />
|
||||
<h4>写出递推式</h4>
|
||||
<p>根据 BFPRT 算法的执行过程,依次与AI教师讨论每步的时间复杂度:
|
||||
<br></p>
|
||||
<ol>
|
||||
<li><strong>将 <eq>n</eq> 个元素分组、找出每组中位数</strong>
|
||||
<ul>
|
||||
<li>分成 <eq>n/5</eq> 组,每组排序并取中位数。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>递归地找所有中位数的中位数(枢轴)</strong>
|
||||
<ul>
|
||||
<li>子问题规模为 <eq>n/5</eq>。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>额外一次线性 partition</strong>
|
||||
<ul>
|
||||
<li>对全数组按枢轴划分:蓝框部分、黑框部分,此外递归时是对非蓝框或非黑框的补集部分。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>将原始数组按枢轴划分,并在一侧继续递归</strong>
|
||||
<ul>
|
||||
<li>对非蓝框或非黑框数进行递归查询。
|
||||
<br></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p><img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" /></p>
|
||||
<p>从而得到最终的递推式。</p>
|
||||
<hr />
|
||||
<h4>证明 <eq>T(n) = O(n)</eq></h4>
|
||||
<p>这里的证明并不复杂,使用高中学习的数学归纳法即可。
|
||||
提示:先假设满足T(n)<=cn;然后证明递推式<=d*cn<=cn,其中d小于1。</p>
|
||||
<p>与AI教师探讨证明流程。</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
Html/apps/static/cdnback/all.min.css
vendored
Normal file
6
Html/apps/static/cdnback/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
Html/apps/static/cdnback/axios.min.js
vendored
Normal file
3
Html/apps/static/cdnback/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2018
Html/apps/static/cdnback/bootstrap-icons.css
vendored
Normal file
2018
Html/apps/static/cdnback/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
Html/apps/static/cdnback/bootstrap.min.css
vendored
Normal file
6
Html/apps/static/cdnback/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3899
Html/apps/static/cdnback/css/bootstrap-grid.css
vendored
Normal file
3899
Html/apps/static/cdnback/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/css/bootstrap-grid.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/css/bootstrap-grid.min.css
vendored
Normal file
7
Html/apps/static/cdnback/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/css/bootstrap-grid.min.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
327
Html/apps/static/cdnback/css/bootstrap-reboot.css
vendored
Normal file
327
Html/apps/static/cdnback/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2019 The Bootstrap Authors
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus:not(:focus-visible) {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[type="button"]:not(:disabled),
|
||||
[type="reset"]:not(:disabled),
|
||||
[type="submit"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
Html/apps/static/cdnback/css/bootstrap-reboot.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
Html/apps/static/cdnback/css/bootstrap-reboot.min.css
vendored
Normal file
8
Html/apps/static/cdnback/css/bootstrap-reboot.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2019 The Bootstrap Authors
|
||||
* Copyright 2011-2019 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
File diff suppressed because one or more lines are too long
10224
Html/apps/static/cdnback/css/bootstrap.css
vendored
Normal file
10224
Html/apps/static/cdnback/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/css/bootstrap.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/css/bootstrap.min.css
vendored
Normal file
7
Html/apps/static/cdnback/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/css/bootstrap.min.css.map
Normal file
1
Html/apps/static/cdnback/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/easymde.min.css
vendored
Normal file
7
Html/apps/static/cdnback/easymde.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/easymde.min.js
vendored
Normal file
7
Html/apps/static/cdnback/easymde.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff
Normal file
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff2
Normal file
BIN
Html/apps/static/cdnback/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
2
Html/apps/static/cdnback/jquery.min.js
vendored
Normal file
2
Html/apps/static/cdnback/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7134
Html/apps/static/cdnback/js/bootstrap.bundle.js
vendored
Normal file
7134
Html/apps/static/cdnback/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/js/bootstrap.bundle.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/js/bootstrap.bundle.min.js
vendored
Normal file
7
Html/apps/static/cdnback/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/js/bootstrap.bundle.min.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4521
Html/apps/static/cdnback/js/bootstrap.js
vendored
Normal file
4521
Html/apps/static/cdnback/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Html/apps/static/cdnback/js/bootstrap.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/js/bootstrap.min.js
vendored
Normal file
7
Html/apps/static/cdnback/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/js/bootstrap.min.js.map
Normal file
1
Html/apps/static/cdnback/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
Html/apps/static/cdnback/markdown-it.min.js
vendored
Normal file
2
Html/apps/static/cdnback/markdown-it.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Html/apps/static/cdnback/marked.min.js
vendored
Normal file
1
Html/apps/static/cdnback/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
Html/apps/static/cdnback/purify.min.js
vendored
Normal file
3
Html/apps/static/cdnback/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Html/apps/static/cdnback/socket.io.min.js
vendored
Normal file
7
Html/apps/static/cdnback/socket.io.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
Html/apps/static/cdnback/split.min.js
vendored
Normal file
3
Html/apps/static/cdnback/split.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -11,42 +11,7 @@ body {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 页眉导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
color: #f1c40f;
|
||||
}
|
||||
|
||||
.navbar .avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
/* Dashboard 主体部分 */
|
||||
|
||||
:root {
|
||||
--primary-blue: #6A9FB5;
|
||||
@@ -371,18 +336,6 @@ body {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
/* 课程卡片样式 */
|
||||
.course-card {
|
||||
border: none;
|
||||
@@ -422,10 +375,18 @@ body {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.course-title-in-progress{
|
||||
color: var(--text-dark);
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.course-instructor {
|
||||
color: var(--text-light);
|
||||
@@ -458,7 +419,7 @@ body {
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.btn-enroll {
|
||||
.btn-process {
|
||||
background-color: var(--primary-blue);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
@@ -470,40 +431,161 @@ body {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
.footer {
|
||||
background-color: var(--text-dark);
|
||||
|
||||
/* Dashboard 主体部分 */
|
||||
.dashboard {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
/* 课程卡片容器 */
|
||||
.course-cards {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 20px;
|
||||
justify-content: flex-start;
|
||||
padding: 10px 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
/* 课程卡片 */
|
||||
.course-card {
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
padding: 2.5rem 0 1.5rem;
|
||||
margin-top: 3rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: auto; /* 将按钮推到父元素的底部 */
|
||||
}
|
||||
|
||||
.footer-heading {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.2rem;
|
||||
color: white;
|
||||
.course-card:hover {
|
||||
transform: translateY(-10px); /* 提升效果 */
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
.course-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.footer-links li {
|
||||
margin-bottom: 0.5rem;
|
||||
.course-info {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: #BDC3C7;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
.course-name {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: white;
|
||||
.course-description {
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
margin-top: 10px;
|
||||
}
|
||||
/* 右侧滑出进度卡片样式 */
|
||||
.course-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px; /* 初始时在屏幕外 */
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px;
|
||||
transition: right 0.3s ease;
|
||||
z-index: 9999;
|
||||
border-radius: 15px; /* 添加圆角 */
|
||||
}
|
||||
|
||||
.course-progress.open {
|
||||
right: 0; /* 打开时显示 */
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* 章节和子章节样式 */
|
||||
.chapter-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chapter-item .chapter-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chapter-item .sub-chapter-list {
|
||||
display: none; /* 初始状态下子章节隐藏 */
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.chapter-item.open .sub-chapter-list {
|
||||
display: block; /* 展开时显示子章节 */
|
||||
}
|
||||
|
||||
.chapter-item:hover {
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 子章节的样式 */
|
||||
.sub-chapter-item {
|
||||
margin: 3px 0;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub-chapter-item:hover {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.path-container {
|
||||
@@ -527,3 +609,8 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.fas.fa-times-circle.fa-lg {
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
@@ -19,12 +19,10 @@ body {
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
}
|
||||
footer {
|
||||
background-color: #f8f9fa;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
/* Header styles */
|
||||
header {
|
||||
background-color: #2575fc;
|
||||
@@ -80,25 +78,9 @@ main {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
|
||||
.select-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
@@ -107,11 +89,6 @@ main {
|
||||
.course-card:hover {
|
||||
transform: translateY(-10px); /* 提升效果 */
|
||||
}
|
||||
.course-card img {
|
||||
width: 100%;
|
||||
height: 261px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.course-card h3 {
|
||||
padding: 15px;
|
||||
@@ -131,9 +108,6 @@ main {
|
||||
}
|
||||
|
||||
.selected-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #8a8b8c;
|
||||
color: white;
|
||||
border: none;
|
||||
|
||||
281
Html/apps/static/css/learning-path.css
Normal file
281
Html/apps/static/css/learning-path.css
Normal file
@@ -0,0 +1,281 @@
|
||||
:root {
|
||||
--primary-blue: #6A9FB5;
|
||||
--secondary-blue: #8BB4C5;
|
||||
--light-blue: #E1F0F7;
|
||||
--accent-blue: #4A7B9D;
|
||||
--text-dark: #2C3E50;
|
||||
--text-light: #7F8C8D;
|
||||
--card-shadow: 0 4px 12px rgba(106, 159, 181, 0.15);
|
||||
}
|
||||
|
||||
/* 学习路径区域 */
|
||||
.learning-path-section {
|
||||
background: linear-gradient(135deg, var(--light-blue), #D4E9F2);
|
||||
padding: 2rem 0;
|
||||
border-radius: 0 0 20px 20px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--accent-blue);
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.section-title:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background-color: var(--primary-blue);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 专业选择器 */
|
||||
.major-selector {
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.major-selector label {
|
||||
font-weight: 600;
|
||||
color: var(--accent-blue);
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.major-selector select {
|
||||
appearance: none;
|
||||
background-color: white;
|
||||
border: 2px solid var(--primary-blue);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text-dark);
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.major-selector select:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(106, 159, 181, 0.3);
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.major-selector::after {
|
||||
content: '\f078';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
/* 学习路径流程图 */
|
||||
.path-flow {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.path-container {
|
||||
display: flex;
|
||||
min-width: 900px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.path-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: -50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: var(--primary-blue);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--light-blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step-node:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.step-node:hover .step-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.8rem;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.step-courses {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
height: 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--primary-blue);
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* 阶段状态样式 */
|
||||
.step-completed .step-node {
|
||||
background-color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.step-completed .step-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-active .step-node {
|
||||
background-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 4px rgba(74, 123, 157, 0.3);
|
||||
}
|
||||
|
||||
.step-active .step-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 课程详情面板 */
|
||||
.course-panel {
|
||||
display: none;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.course-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: 600;
|
||||
color: var(--accent-blue);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-panel {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.course-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.course-item:hover {
|
||||
border-color: var(--primary-blue);
|
||||
box-shadow: 0 2px 8px rgba(106, 159, 181, 0.15);
|
||||
}
|
||||
|
||||
.course-check {
|
||||
margin-right: 0.75rem;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.course-duration {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.course-action {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
@@ -125,7 +125,7 @@
|
||||
.tab-btn.active { background:#f0f8ff; border-color:#a0c4ff; }
|
||||
.actions { margin-left:auto; }
|
||||
.actions button { padding:6px 12px; border:none; border-radius:8px; background:#4CAF50; color:#fff; cursor:pointer; }
|
||||
.tab-content { flex:1; padding:8px; }
|
||||
.tab-content { flex:1; padding:8px;display: flex; }
|
||||
.tab { display:none; height:100%; }
|
||||
.tab.active { display:block; height:100%; }
|
||||
textarea {
|
||||
|
||||
63
Html/apps/static/css/lesson_markdown.css
Normal file
63
Html/apps/static/css/lesson_markdown.css
Normal file
@@ -0,0 +1,63 @@
|
||||
/* 让左侧编辑区真正占满并允许内部滚动 */
|
||||
.tab-content {
|
||||
height: calc(100vh - 220px);
|
||||
/* 不要隐藏滚动:*/
|
||||
overflow: visible; /* 或者干脆删掉 overflow 这行 */
|
||||
}
|
||||
|
||||
/* EasyMDE 容器按列铺开,编辑器本体撑满剩余空间 */
|
||||
.EasyMDEContainer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 关键:让 CodeMirror 吃满容器高度 */
|
||||
.EasyMDEContainer .CodeMirror {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 关键:滚动发生在 CodeMirror-scroll 里 */
|
||||
.EasyMDEContainer .CodeMirror-scroll {
|
||||
height: 100% !important;
|
||||
overflow: auto !important; /* 启用内部滚动条(也支持鼠标滚轮) */
|
||||
}
|
||||
|
||||
/* 右侧预览保持原样独立滚动 */
|
||||
.md-preview-pane {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.md-preview-pane h1, .md-preview-pane h2, .md-preview-pane h3 {
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding-bottom: .3em;
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
.md-preview-pane img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
/* 只影响右侧 Markdown 预览里的表格 */
|
||||
.md-preview-pane .table {
|
||||
/* 如果你想完全摆脱 bootstrap.table 的影响 */
|
||||
all: revert; /* 或者 all: unset; 视需求选择 */
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
/* 下面写你自己的表格样式 */
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.md-preview-pane .table th,
|
||||
.md-preview-pane .table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.md-preview-pane .table thead th {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1,25 +1,19 @@
|
||||
/* 页眉导航栏 基础样式 */
|
||||
/* 页眉导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, var(--primary-blue), var(--accent-blue));
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
/* 导航栏 Logo 样式 */
|
||||
.navbar .logo h1 {
|
||||
font-size: 20px;
|
||||
color: rgb(245, 245, 245);
|
||||
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 导航链接样式 */
|
||||
.navbar-links a {
|
||||
color: #4b5563;
|
||||
color: rgb(235, 235, 235);
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
font-size: 18px;
|
||||
@@ -27,21 +21,18 @@
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
color: #2575fc;
|
||||
}
|
||||
|
||||
/* 头像容器样式 */
|
||||
.navbar .avatar {
|
||||
margin-left: 0;
|
||||
color: #f1c40f;
|
||||
}
|
||||
|
||||
.navbar .avatar img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 下拉浮框 基础定位 */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
|
||||
12
Html/apps/static/css/search-section.css
Normal file
12
Html/apps/static/css/search-section.css
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
@@ -13,7 +13,39 @@
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#chapter-list {
|
||||
/* 设置固定高度,超出范围将出现滚动 */
|
||||
max-height: 75vh;
|
||||
/* 超出高度时显示垂直滚动条 */
|
||||
overflow-y: auto;
|
||||
|
||||
/* 隐藏水平滚动条(如果不需要) */
|
||||
overflow-x: hidden;
|
||||
|
||||
/* 可选:添加一些内边距和边框增强视觉效果 */
|
||||
padding: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 可选:美化滚动条样式(仅WebKit浏览器有效) */
|
||||
#chapter-list::-webkit-scrollbar {
|
||||
width: 6px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
#chapter-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1; /* 滚动条轨道背景 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#chapter-list::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1; /* 滚动条滑块颜色 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#chapter-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8; /* 鼠标悬停时的滑块颜色 */
|
||||
}
|
||||
.course-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
BIN
Html/apps/static/favicon.ico
Normal file
BIN
Html/apps/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 738 KiB |
@@ -1,10 +1,24 @@
|
||||
|
||||
var socket;
|
||||
let system_message_idx = 0
|
||||
const activeApprovals = {};
|
||||
// 清理过期的批准记录
|
||||
function cleanupExpiredApprovals() {
|
||||
const now = Date.now();
|
||||
for (const name in activeApprovals) {
|
||||
if (now - activeApprovals[name] > 30000) { // 30秒过期
|
||||
delete activeApprovals[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定时清理过期记录
|
||||
setInterval(cleanupExpiredApprovals, 3000); // 每3秒检查一次
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
data = window.appData;
|
||||
console.log(data);
|
||||
socket = io('/agent', {
|
||||
const host = window.location.host;
|
||||
console.log(`Connecting to WebSocket server at wss://${host}/agent`);
|
||||
socket = io(`wss://${host}/agent`, {
|
||||
query: {
|
||||
user_uuid: data.user_uuid,
|
||||
folder: data.folder
|
||||
@@ -51,14 +65,32 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof data === 'object') {
|
||||
data = [data];
|
||||
}
|
||||
cleanupExpiredApprovals();// 先清理一次过期记录
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const element = data[i];
|
||||
const funcName = element.data.name;
|
||||
|
||||
if (activeApprovals.hasOwnProperty(funcName)) {
|
||||
// 如果存在,找到并移除之前的元素
|
||||
const existingElements = document.querySelectorAll(`.approve-message[data-function="${funcName}"]`);
|
||||
existingElements.forEach(el => el.remove());
|
||||
}
|
||||
activeApprovals[funcName] = Date.now();
|
||||
|
||||
const requestMessage = document.createElement('div');
|
||||
requestMessage.className = 'chat-message server-message approve-message';
|
||||
requestMessage.setAttribute('data-function', funcName);
|
||||
|
||||
const description = document.createElement('div');
|
||||
description.innerHTML = `<b>Function:</b> ${element.data.name}(${JSON.stringify(element.data.args)})`;
|
||||
const argsDescription = JSON.stringify(element.data.args);
|
||||
if (argsDescription) {
|
||||
description.innerHTML = `<b>Function:</b> ${element.data.name}(${argsDescription})`;
|
||||
}else {
|
||||
description.innerHTML = `<b>Function:</b> ${element.data.name}()`;
|
||||
}
|
||||
|
||||
const approveButton = document.createElement('button');
|
||||
approveButton.innerHTML = '√'; // 使用勾选符号
|
||||
approveButton.innerHTML = '<i class="fas fa-check-circle-check"></i>';
|
||||
approveButton.data = element;
|
||||
approveButton.className = 'approve-button';
|
||||
approveButton.onclick = function (event) {
|
||||
@@ -73,29 +105,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('chatbox').appendChild(requestMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
// for (let i = 0; i < data.length; i++) {
|
||||
// const element = data[i]
|
||||
// const requestMessage = document.createElement('div');
|
||||
// requestMessage.className = 'chat-message server-message';
|
||||
// const approveButton = document.createElement('button');
|
||||
// approveButton.innerText = '批准';
|
||||
// approveButton.data = element;
|
||||
// approveButton.style.width = '100%';
|
||||
// approveButton.style.borderRadius = '4px';
|
||||
// approveButton.onclick = function(event) {
|
||||
// console.log(element)
|
||||
// socket.emit('message', JSON.stringify({type:'function', data:element}));
|
||||
// event.currentTarget.disabled = true;
|
||||
// event.currentTarget.style.backgroundColor = 'gray';
|
||||
// event.currentTarget.style.color = 'white'; // 可选:设置文字颜色为白色,以便更清晰地显示
|
||||
// event.currentTarget.style.border = 'none'; // 可选:去掉边框
|
||||
// };
|
||||
// // 将按钮添加到消息气泡中
|
||||
// requestMessage.innerHTML = `<b>Function:</b> ${element.name}(${JSON.stringify(element.arguments)})<br>`;
|
||||
// requestMessage.appendChild(approveButton);
|
||||
// document.getElementById('chatbox').appendChild(requestMessage);
|
||||
// document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
// }
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -218,124 +228,9 @@ function sendMessage() {
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
|
||||
// let voice_stream = null;
|
||||
// let audio_chunks = [];
|
||||
// let isRecording = false;
|
||||
// let recordingInterval = null;
|
||||
// //鼠标点击终止事件
|
||||
// function handleMouseClick(event) {
|
||||
// event.stopPropagation();
|
||||
// stopRecording();
|
||||
// }
|
||||
// //发送语音
|
||||
// const startAudioRecording = async () => {
|
||||
// try {
|
||||
// const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// audio: {
|
||||
// sampleRate: 16000,
|
||||
// channelCount: 1,
|
||||
// echoCancellation: true,
|
||||
// noiseSuppression: true
|
||||
// }
|
||||
// });
|
||||
// const mediaRecorder = new MediaRecorder(stream);
|
||||
// const audioContext = new AudioContext({
|
||||
// sampleRate: 16000
|
||||
// });
|
||||
|
||||
// // Load the AudioWorklet
|
||||
// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
|
||||
|
||||
// const source = audioContext.createMediaStreamSource(stream);
|
||||
// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
|
||||
// let audioChunks = [];
|
||||
|
||||
// workletNode.port.onmessage = (e) => {
|
||||
// audioChunks.push(new Float32Array(e.data));
|
||||
|
||||
// if (audioChunks.length >= 16) {
|
||||
// // 合并所有音频块
|
||||
// const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
// const mergedData = new Float32Array(totalLength);
|
||||
// let offset = 0;
|
||||
// for (const chunk of audioChunks) {
|
||||
// mergedData.set(chunk, offset);
|
||||
// offset += chunk.length;
|
||||
// }
|
||||
|
||||
// const audioMessage = {
|
||||
// id: window.data.user_uuid,
|
||||
// type: 'audio',
|
||||
// data: mergedData,
|
||||
// sampleRate: 16000,
|
||||
// timestamp: Date.now()
|
||||
// };
|
||||
// socket.emit('voice', audioMessage);
|
||||
// console.log("voice sended")
|
||||
// audioChunks = [];
|
||||
// }
|
||||
// };
|
||||
|
||||
// source.connect(workletNode);
|
||||
// workletNode.connect(audioContext.destination);
|
||||
|
||||
// mediaRecorder.value = {
|
||||
// stop: () => {
|
||||
// source.disconnect();
|
||||
// workletNode.disconnect();
|
||||
// stream.getTracks().forEach(track => track.stop());
|
||||
// audioContext.close();
|
||||
// }
|
||||
// }
|
||||
|
||||
// isRecording.value = true;
|
||||
// } catch (error) {
|
||||
// console.error('Error starting audio recording:', error);
|
||||
// }
|
||||
// };
|
||||
// function sendVoiceMessage() {
|
||||
// startAudioRecording()
|
||||
// //变换录音中图标
|
||||
// document.getElementById('recordIcon').className = 'bi bi-record-circle';
|
||||
// document.addEventListener('click', handleMouseClick);
|
||||
// }
|
||||
|
||||
// //停止录音
|
||||
// function stopRecording() {
|
||||
// if (!isRecording) return;
|
||||
// isRecording = false;
|
||||
// //取消监听
|
||||
// document.removeEventListener('click', handleMouseClick);
|
||||
// //取消定时器
|
||||
// if (recordingInterval) {
|
||||
// clearInterval(recordingInterval);
|
||||
// recordingInterval = null;
|
||||
// }
|
||||
// //变换图标
|
||||
// document.getElementById('recordIcon').className = 'bi bi-mic';
|
||||
// //发送剩下的数据
|
||||
// if (audio_chunks.length > 0) {
|
||||
// const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0));
|
||||
// let offset = 0;
|
||||
// for (const chunk of audio_chunks) {
|
||||
// merged_data.set(new Uint8Array(chunk), offset);
|
||||
// offset += chunk.length;
|
||||
// }
|
||||
// socket.emit('VoiceMessage', merged_data, { binary: true });
|
||||
// audio_chunks = [];
|
||||
// }
|
||||
// overlay.style.display = 'none';
|
||||
// //关闭音频流
|
||||
// if (voice_stream) {
|
||||
// voice_stream.stop_stream();
|
||||
// voice_stream.close();
|
||||
// voice_stream = null;
|
||||
// }
|
||||
// if (p) {
|
||||
// p.terminate();
|
||||
// p = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
//语音这一块//
|
||||
|
||||
@@ -143,4 +143,11 @@ function closeCourseProgress() {
|
||||
|
||||
function isObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]';
|
||||
}
|
||||
|
||||
function btn_view(event, courseId) {
|
||||
event.stopPropagation(); // 阻止事件冒泡
|
||||
// 打开课程详情页面
|
||||
const url = `/course/${courseId}`;
|
||||
window.location.href = url;
|
||||
}
|
||||
@@ -6,37 +6,36 @@ function show_course_details(course_id){
|
||||
window.location.href = '/course/' + course_id
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectButtons = document.querySelectorAll('.select-button');
|
||||
|
||||
selectButtons.forEach(button => {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.stopPropagation(); // 阻止事件冒泡
|
||||
const courseId = this.getAttribute('data-course-id');
|
||||
fetch(`/select_course`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ course_id: courseId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 更新按钮样式或显示消息
|
||||
this.classList.remove('select-button');
|
||||
this.classList.add('selected-button');
|
||||
this.textContent = '已选择';
|
||||
alert('选择课程成功');
|
||||
} else {
|
||||
alert('选择课程失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('选择课程失败');
|
||||
});
|
||||
});
|
||||
function on_click_select_course(event){
|
||||
event.stopPropagation(); // 阻止事件冒泡
|
||||
const courseId = event.currentTarget.getAttribute('data-course-id');
|
||||
if (event.currentTarget.classList.contains("selected-button")) {alert('课程已选择'); return;}
|
||||
fetch(`/select_course`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ course_id: courseId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 更新按钮样式或显示消息
|
||||
this.classList.remove('select-button');
|
||||
this.classList.add('selected-button');
|
||||
this.textContent = '已选择';
|
||||
alert('选择课程成功');
|
||||
} else {
|
||||
alert('选择课程失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('选择课程失败');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
});
|
||||
30
Html/apps/static/js/learning-path.js
Normal file
30
Html/apps/static/js/learning-path.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// 1. 点击学习阶段节点,显示对应课程面板
|
||||
document.querySelectorAll('.step-node').forEach((node) => {
|
||||
node.addEventListener('click', function () {
|
||||
// 隐藏所有面板
|
||||
document.querySelectorAll('.course-panel').forEach(panel => {
|
||||
panel.classList.remove('active');
|
||||
});
|
||||
// 获取当前阶段名称,匹配面板ID
|
||||
const stepTitle = this.closest('.path-step').querySelector('.step-title').textContent;
|
||||
const panelId = stepTitle.toLowerCase().replace(' ', '-') + '-panel';
|
||||
const targetPanel = document.getElementById(panelId);
|
||||
// 显示对应面板(若存在)
|
||||
if (targetPanel) {
|
||||
targetPanel.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 关闭课程面板
|
||||
document.querySelectorAll('.close-panel').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
this.closest('.course-panel').classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 切换专业方向(可扩展为动态更新学习路径)
|
||||
document.getElementById('majorSelect').addEventListener('change', function () {
|
||||
const selectedMajor = this.options[this.selectedIndex].text;
|
||||
// 实际项目中可替换为AJAX请求,加载对应专业的学习路径
|
||||
});
|
||||
@@ -801,10 +801,11 @@ function ValidTextareaNoBlankHead(textarea) {
|
||||
// 更新 textarea 的值(如果有变化)
|
||||
textarea.value = lines.join("\n");
|
||||
}
|
||||
const editors = document.querySelectorAll("textarea");
|
||||
const editorsDoc = document.querySelectorAll("textarea");
|
||||
const PREFIX = "### ";
|
||||
// 记录每个 textarea 最新的“标题内容”(不含前缀)
|
||||
const lastTitleMap = new Map();
|
||||
|
||||
// 防止循环触发
|
||||
let isProgrammaticUpdate = false;
|
||||
function getFirstLine(text) {
|
||||
@@ -823,7 +824,7 @@ const lastTitleMap = new Map();
|
||||
return { line: normalized, title };
|
||||
}
|
||||
// 初始化 lastTitleMap(把当前值规范化一次)
|
||||
editors.forEach(ed => {
|
||||
editorsDoc.forEach(ed => {
|
||||
const rawFirst = getFirstLine(ed.value || "");
|
||||
const { line, title } = normalizeFirstLine(rawFirst);
|
||||
if (line !== rawFirst) {
|
||||
@@ -837,7 +838,7 @@ const lastTitleMap = new Map();
|
||||
function syncOthers(sourceEditor, newTitle) {
|
||||
isProgrammaticUpdate = true;
|
||||
try {
|
||||
editors.forEach(ed => {
|
||||
editorsDoc.forEach(ed => {
|
||||
if (ed === sourceEditor) return;
|
||||
const first = getFirstLine(ed.value || "");
|
||||
const { line } = normalizeFirstLine(first);
|
||||
@@ -851,56 +852,204 @@ const lastTitleMap = new Map();
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
editors.forEach(editor => {
|
||||
editor.addEventListener("input", (event) => {
|
||||
const textarea = event.target
|
||||
const value = event.target.value;
|
||||
const type = event.target.dataset.type;
|
||||
// 在这里做提示或校验
|
||||
console.log(`内容已更改: ${event.target.id} => ${value}`);
|
||||
|
||||
// ==== 前置:你的三编辑器实例与 id 映射(按你实际为准)====
|
||||
const ID_LESSON = 'editor-lesson';
|
||||
const ID_PROMPT = 'editor-prompt';
|
||||
const ID_SCORE = 'editor-score';
|
||||
|
||||
ValidTextareaNoBlankHead(event.target)
|
||||
// editors: { [textareaId]: EasyMDEInstance }
|
||||
const editorIds = [ID_LESSON, ID_PROMPT, ID_SCORE];
|
||||
|
||||
const rawFirst = getFirstLine(value);
|
||||
// 全局防抖/重入保护:避免程序化更新再触发同步
|
||||
|
||||
// // 记录上一次各编辑器的标题文本(不含 "### " 前缀)
|
||||
// const lastTitleMap = new Map();
|
||||
|
||||
// —— 工具:从 EasyMDE 拿值 / 设值(会同步 textarea,因为 forceSync:true)——
|
||||
const getMD = (id) => editors[id].value();
|
||||
const setMD = (id, text) => editors[id].value(text);
|
||||
|
||||
// —— 工具:仅替换“首行”为 normalizedLine,尽量不破坏光标与撤销栈 ——
|
||||
// line 是完整首行文本(含 "### ")
|
||||
function setFirstLineInEditor(ed, normalizedLine) {
|
||||
const cm = ed.codemirror;
|
||||
cm.operation(() => {
|
||||
const doc = cm.getDoc();
|
||||
const firstLine = doc.getLine(0) ?? "";
|
||||
if (firstLine === normalizedLine) return;
|
||||
|
||||
// 记住用户光标
|
||||
const cursors = doc.listSelections();
|
||||
|
||||
// 用 replaceRange 仅替换首行
|
||||
doc.replaceRange(
|
||||
normalizedLine,
|
||||
{ line: 0, ch: 0 },
|
||||
{ line: 0, ch: firstLine.length }
|
||||
);
|
||||
|
||||
// 尽力恢复光标(如果原先在首行,偏移可能要校正)
|
||||
doc.setSelections(cursors);
|
||||
});
|
||||
}
|
||||
|
||||
// —— 工具:从 Markdown 文本取首行(兼容你现有函数)
|
||||
function getFirstLineFromText(text) {
|
||||
const idx = text.indexOf('\n');
|
||||
return idx === -1 ? text : text.slice(0, idx);
|
||||
}
|
||||
|
||||
// —— 工具:把 normalizedLine 写回文本首行(备用:整段 setValue)——
|
||||
function setFirstLineInText(text, normalizedLine) {
|
||||
const idx = text.indexOf('\n');
|
||||
return idx === -1 ? normalizedLine : (normalizedLine + text.slice(idx));
|
||||
}
|
||||
|
||||
// —— 同步另外两个编辑器的首行(仅标题行),避免全文覆盖 ——
|
||||
function syncOthersTitle(fromId, titleText /*不含前缀*/, normalizedLine /*含### */) {
|
||||
editorIds.forEach((id) => {
|
||||
if (id === fromId) return;
|
||||
const ed = editors[id];
|
||||
if (!ed) return;
|
||||
|
||||
const currentFirst = ed.codemirror.getDoc().getLine(0) ?? "";
|
||||
if (currentFirst !== normalizedLine) {
|
||||
setFirstLineInEditor(ed, normalizedLine);
|
||||
lastTitleMap.set(ed, titleText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// —— 更新 tab 按钮上的 “*” 修改状态(保持你原逻辑)——
|
||||
function refreshTabDirtyBadges() {
|
||||
let btn = document.getElementById("tab-btn-lesson");
|
||||
if (CURRENT.active.lesson !== getMD(ID_LESSON)) {
|
||||
btn.innerText = AddModifiedStat(btn.innerText);
|
||||
} else {
|
||||
btn.innerText = ClearModifiedStat(btn.innerText);
|
||||
}
|
||||
|
||||
btn = document.getElementById("tab-btn-prompt");
|
||||
if (CURRENT.active.prompt !== getMD(ID_PROMPT)) {
|
||||
btn.innerText = AddModifiedStat(btn.innerText);
|
||||
} else {
|
||||
btn.innerText = ClearModifiedStat(btn.innerText);
|
||||
}
|
||||
|
||||
btn = document.getElementById("tab-btn-score");
|
||||
if (CURRENT.active.score !== getMD(ID_SCORE)) {
|
||||
btn.innerText = AddModifiedStat(btn.innerText);
|
||||
} else {
|
||||
btn.innerText = ClearModifiedStat(btn.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
// —— 单个编辑器的 change 处理器工厂 ——
|
||||
function bindEditorChangeHandler(textareaId) {
|
||||
const ed = editors[textareaId];
|
||||
const cm = ed.codemirror;
|
||||
|
||||
// 初始化 lastTitleMap
|
||||
lastTitleMap.set(ed, "");
|
||||
|
||||
cm.on('change', () => {
|
||||
if (isProgrammaticUpdate) return;
|
||||
|
||||
const value = ed.value();
|
||||
const rawFirst = getFirstLineFromText(value);
|
||||
|
||||
// 你自己的校验(如果要继续保留)
|
||||
// ValidTextareaNoBlankHead(ed.element.nextSibling /* textarea DOM? */);
|
||||
|
||||
// 归一化首行,要求 "### " 前缀
|
||||
const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
|
||||
|
||||
// 若首行不规范,程序化改回去(仅替换首行)
|
||||
if (normalizedLine !== rawFirst) {
|
||||
isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
|
||||
isProgrammaticUpdate = true;
|
||||
try {
|
||||
textarea.value = setFirstLine(value, normalizedLine);
|
||||
setFirstLineInEditor(ed, normalizedLine);
|
||||
} finally {
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
|
||||
|
||||
// 标题变化则同步到其他两个编辑器(只同步首行)
|
||||
const lastTitle = lastTitleMap.get(ed) ?? "";
|
||||
if (currentTitle !== lastTitle) {
|
||||
lastTitleMap.set(textarea, currentTitle);// 更新自身记录
|
||||
syncOthers(textarea, currentTitle);
|
||||
lastTitleMap.set(ed, currentTitle);
|
||||
isProgrammaticUpdate = true;
|
||||
try {
|
||||
syncOthersTitle(textareaId, currentTitle, normalizedLine);
|
||||
} finally {
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
let btn = document.getElementById("tab-btn-lesson");
|
||||
if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
|
||||
btn.innerText = AddModifiedStat(btn.innerText)
|
||||
}else{
|
||||
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
}
|
||||
btn = document.getElementById("tab-btn-prompt");
|
||||
if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
|
||||
btn.innerText = AddModifiedStat(btn.innerText)
|
||||
}else{
|
||||
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
}
|
||||
btn = document.getElementById("tab-btn-score");
|
||||
if (CURRENT.active.score != document.getElementById("editor-score").value){
|
||||
btn.innerText = AddModifiedStat(btn.innerText)
|
||||
}else{
|
||||
btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// 刷新三个 Tab 的“修改中 * ”标识
|
||||
refreshTabDirtyBadges();
|
||||
|
||||
// 如果你还有右侧预览:
|
||||
if (typeof renderToPreview === 'function' && textareaId === activeId) {
|
||||
renderToPreview(ed.value());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定三个编辑器
|
||||
editorIds.forEach(bindEditorChangeHandler);
|
||||
|
||||
// editorsDoc.forEach(editor => {
|
||||
// editor.addEventListener("input", (event) => {
|
||||
// const textarea = event.target
|
||||
// const value = event.target.value;
|
||||
// const type = event.target.dataset.type;
|
||||
// // 在这里做提示或校验
|
||||
// console.log(`内容已更改: ${event.target.id} => ${value}`);
|
||||
|
||||
// ValidTextareaNoBlankHead(event.target)
|
||||
|
||||
// const rawFirst = getFirstLine(value);
|
||||
// const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
|
||||
|
||||
// if (normalizedLine !== rawFirst) {
|
||||
// isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
|
||||
// try {
|
||||
// textarea.value = setFirstLine(value, normalizedLine);
|
||||
// } finally {
|
||||
// isProgrammaticUpdate = false;
|
||||
// }
|
||||
// }
|
||||
// const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
|
||||
// if (currentTitle !== lastTitle) {
|
||||
// lastTitleMap.set(textarea, currentTitle);// 更新自身记录
|
||||
// syncOthers(textarea, currentTitle);
|
||||
// }
|
||||
|
||||
// let btn = document.getElementById("tab-btn-lesson");
|
||||
// if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
|
||||
// btn.innerText = AddModifiedStat(btn.innerText)
|
||||
// }else{
|
||||
// btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// }
|
||||
// btn = document.getElementById("tab-btn-prompt");
|
||||
// if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
|
||||
// btn.innerText = AddModifiedStat(btn.innerText)
|
||||
// }else{
|
||||
// btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// }
|
||||
// btn = document.getElementById("tab-btn-score");
|
||||
// if (CURRENT.active.score != document.getElementById("editor-score").value){
|
||||
// btn.innerText = AddModifiedStat(btn.innerText)
|
||||
// }else{
|
||||
// btn.innerText = ClearModifiedStat(btn.innerText)
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
// 把“某个步骤”对应的三个片段载入到三个编辑器
|
||||
// 把“某个步骤”对应的三个片段载入到三个编辑器
|
||||
function loadStepToEditors(phaseText, stepText) {
|
||||
// 三份文档里都取对应 phase/step 的片段(不存在则回退)
|
||||
const L = CURRENT.parsed.lesson, P = CURRENT.parsed.prompt, S = CURRENT.parsed.score;
|
||||
@@ -918,9 +1067,9 @@ function loadStepToEditors(phaseText, stepText) {
|
||||
const pText = sliceSection(subP, stepText, 3) || promptPhase;
|
||||
const sText = sliceSection(subS, stepText, 3) || scorePhase;
|
||||
|
||||
document.getElementById('editor-lesson').value = lText;
|
||||
document.getElementById('editor-prompt').value = pText;
|
||||
document.getElementById('editor-score').value = sText;
|
||||
editors['editor-lesson'].value(lText);
|
||||
editors['editor-prompt'].value(pText);
|
||||
editors['editor-score'].value(sText);
|
||||
|
||||
CURRENT.active={ //保存打开时的三个文本
|
||||
phase:phaseText,
|
||||
@@ -938,16 +1087,32 @@ function loadStepToEditors(phaseText, stepText) {
|
||||
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
document.addEventListener('click', (e)=>{
|
||||
if (e.target.classList.contains('tab-btn')) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
const id = e.target.dataset.target;
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
document.getElementById(id).classList.add('active');
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('tab-btn')) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
|
||||
const id = e.target.dataset.target;
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById(id).classList.add('active');
|
||||
|
||||
// 广播 tab 变更(告诉别人当前激活的 tab id / 以及要用哪个 editorId)
|
||||
const editorIdMap = {
|
||||
'tab-lesson' : 'editor-lesson',
|
||||
'tab-prompt' : 'editor-prompt',
|
||||
'tab-score' : 'editor-score',
|
||||
};
|
||||
const activeBtnId = e.target.id; // 如 'tab-btn-lesson'
|
||||
const editorId =
|
||||
activeBtnId === 'tab-btn-lesson' ? 'editor-lesson' :
|
||||
activeBtnId === 'tab-btn-prompt' ? 'editor-prompt' :
|
||||
'editor-score';
|
||||
|
||||
document.dispatchEvent(new CustomEvent('tab:change', {
|
||||
detail: { tabId: id, editorId }
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
|
||||
document.getElementById('save-all-btn')?.addEventListener('click', async () => {
|
||||
|
||||
198
Html/apps/static/js/lesson_markdown.js
Normal file
198
Html/apps/static/js/lesson_markdown.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const editors = {}; // id -> EasyMDE
|
||||
(function(){
|
||||
// ====== 配置:图片上传接口 ======
|
||||
const UPLOAD_URL = '/api/upload'; // 返回 { url: 'https://...' }
|
||||
|
||||
// ====== Markdown 渲染器 ======
|
||||
const md = window.markdownit({
|
||||
html: false, // 禁止原生HTML,安全些(如需支持可设为 true)
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
});
|
||||
|
||||
const $preview = document.getElementById('md-preview');
|
||||
|
||||
// ====== 三个 textarea 的 EasyMDE 实例 ======
|
||||
const textareas = [
|
||||
{ id: 'editor-lesson', tabBtnId: 'tab-btn-lesson' },
|
||||
{ id: 'editor-prompt', tabBtnId: 'tab-btn-prompt' },
|
||||
{ id: 'editor-score', tabBtnId: 'tab-btn-score' },
|
||||
];
|
||||
|
||||
let activeId = 'editor-lesson'; // 默认激活
|
||||
|
||||
// 工具:将 Markdown 渲染到右侧预览
|
||||
const renderToPreview = (markdownStr) => {
|
||||
const rawHtml = md.render(markdownStr || '');
|
||||
const safe = DOMPurify.sanitize(rawHtml);
|
||||
$preview.innerHTML = safe;
|
||||
};
|
||||
|
||||
// 工具:节流/防抖
|
||||
function debounce(fn, delay){
|
||||
let t;
|
||||
return function(...args){
|
||||
clearTimeout(t);
|
||||
t = setTimeout(()=>fn.apply(this,args), delay);
|
||||
}
|
||||
}
|
||||
const debouncedPreview = debounce(renderToPreview, 120);
|
||||
|
||||
// 工具:插入文本到当前光标
|
||||
function insertAtCursor(cm, text){
|
||||
const doc = cm.getDoc();
|
||||
const cursor = doc.getCursor();
|
||||
doc.replaceRange(text, cursor);
|
||||
}
|
||||
|
||||
// 图片上传:给粘贴/拖拽/按钮选择共用
|
||||
async function uploadFile(file){
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const { data } = await axios.post(UPLOAD_URL, form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
if(!data || !data.url){
|
||||
throw new Error('上传返回缺少 url 字段');
|
||||
}
|
||||
return data.url;
|
||||
}
|
||||
|
||||
// 为一个 EasyMDE 实例注入图片粘贴/拖拽/按钮上传
|
||||
function bindImageHandlers(easy){
|
||||
const cm = easy.codemirror;
|
||||
|
||||
// 1) 粘贴图片
|
||||
cm.on('paste', async (cmInst, e)=>{
|
||||
const items = (e.clipboardData || e.originalEvent?.clipboardData)?.items || [];
|
||||
for(const it of items){
|
||||
if(it.kind === 'file'){
|
||||
const file = it.getAsFile();
|
||||
if(file && file.type.startsWith('image/')){
|
||||
e.preventDefault();
|
||||
try{
|
||||
const url = await uploadFile(file);
|
||||
insertAtCursor(cmInst, ``);
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('图片上传失败,请重试');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2) 拖拽图片
|
||||
easy.codemirror.getWrapperElement().addEventListener('drop', async (e)=>{
|
||||
const files = e.dataTransfer?.files || [];
|
||||
if(!files.length) return;
|
||||
const img = Array.from(files).find(f=>f.type.startsWith('image/'));
|
||||
if(!img) return;
|
||||
e.preventDefault();
|
||||
try{
|
||||
const url = await uploadFile(img);
|
||||
insertAtCursor(cm, ``);
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('图片上传失败,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 3) 工具栏“上传图片”按钮:使用隐藏 input[type=file]
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.addEventListener('change', async ()=>{
|
||||
const file = fileInput.files?.[0];
|
||||
if(!file) return;
|
||||
try{
|
||||
const url = await uploadFile(file);
|
||||
insertAtCursor(cm, ``);
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('图片上传失败,请重试');
|
||||
}finally{
|
||||
fileInput.value = '';
|
||||
}
|
||||
});
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
// 将 input 触发器挂到 toolbar 按钮上(下面初始化里有 name: 'upload-image')
|
||||
easy.toolbar?.forEach(btn=>{
|
||||
if(btn?.name === 'upload-image'){
|
||||
btn.action = () => fileInput.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 创建并初始化一个 EasyMDE
|
||||
function createMDEFor(textareaId){
|
||||
const el = document.getElementById(textareaId);
|
||||
if(!el) return null;
|
||||
|
||||
const mde = new EasyMDE({
|
||||
element: el,
|
||||
autoDownloadFontAwesome: false,
|
||||
spellChecker: false,
|
||||
status: false,
|
||||
autofocus: textareaId === activeId,
|
||||
minHeight: '100%',
|
||||
renderingConfig: { singleLineBreaks: false },
|
||||
uploadImage: false, // 我们自定义上传
|
||||
toolbar: [
|
||||
'bold','italic','heading','|',
|
||||
'quote','unordered-list','ordered-list','table','|',
|
||||
'code','link',
|
||||
{ name:'upload-image', action:null, className:'fa fa-image', title:'插入图片(上传)' },
|
||||
'|','preview','side-by-side','fullscreen',
|
||||
'|','guide'
|
||||
],
|
||||
// 保证 textarea 同步(默认就会同步 value;保存时你的原逻辑仍可读取)
|
||||
forceSync: true
|
||||
});
|
||||
|
||||
// 内容变化 -> 刷新预览(仅激活编辑器触发)
|
||||
mde.codemirror.on('change', ()=>{
|
||||
if(textareaId === activeId){
|
||||
debouncedPreview(mde.value());
|
||||
}
|
||||
});
|
||||
|
||||
bindImageHandlers(mde);
|
||||
return mde;
|
||||
}
|
||||
|
||||
// 初始化三个编辑器
|
||||
for(const t of textareas){
|
||||
editors[t.id] = createMDEFor(t.id);
|
||||
}
|
||||
|
||||
// 初次渲染预览
|
||||
debouncedPreview(editors[activeId]?.value() || '');
|
||||
|
||||
// 监听 Tab 切换按钮,让预览跟随当前激活编辑器
|
||||
function setActiveEditor(id){
|
||||
activeId = id;
|
||||
// 确保 EasyMDE 的编辑区获得焦点(可选)
|
||||
const ed = editors[id];
|
||||
if(ed){
|
||||
ed.codemirror.refresh();
|
||||
ed.codemirror.focus();
|
||||
renderToPreview(ed.value());
|
||||
}
|
||||
}
|
||||
document.addEventListener('tab:change', (e) => {
|
||||
const { editorId } = e.detail || {};
|
||||
if (editorId) setActiveEditor(editorId);
|
||||
});
|
||||
|
||||
// 如果你已有 tab 切换逻辑(例如给 .tab-btn 切换 active 类),保留原逻辑即可;
|
||||
// 我们只需在切换时调用 setActiveEditor 对齐预览内容。
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -450,6 +450,7 @@ async function persistOrderNow(chapters) {
|
||||
});
|
||||
if (!res.ok) throw new Error('保存新顺序失败');
|
||||
console.log('已保存顺序')
|
||||
console.log(payload);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
BIN
Html/apps/static/webfonts/fa-regular-400.woff2
Normal file
BIN
Html/apps/static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
Html/apps/static/webfonts/fa-solid-900.woff2
Normal file
BIN
Html/apps/static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
@@ -11,12 +11,18 @@
|
||||
<script>
|
||||
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/"></script> -->
|
||||
<script src="/static/cdnback/marked.min.js"></script>
|
||||
<!-- <script src="https://cdn.socket.io/4.0.0/"></script> -->
|
||||
<script src="/static/cdnback/socket.io.min.js"></script>
|
||||
<!-- <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/" rel="stylesheet"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/bootstrap.min.css">
|
||||
<!-- <script src="https://unpkg.com/split.js/dist/"></script> -->
|
||||
<script src="/static/cdnback/split.min.js"></script>
|
||||
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/"></script> -->
|
||||
<script src="/static/cdnback/jquery.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/bootstrap-icons.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="maxcontainer" id="maxcontainer" style="height: 100%;">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<!-- 上半部分:课程封面和详情 -->
|
||||
<div class="course-overview">
|
||||
<div class="course-cover">
|
||||
<img src="{{course_data.course_img_path}}" alt="课程封面" class="cover-image">
|
||||
<img src="{{course_data.image_url}}" alt="课程封面" class="cover-image">
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<h2 class="course-title">{{course_data.course_name}}</h2>
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>华实学伴 - 专业的在线学习平台</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> -->
|
||||
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome 图标 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/search-section.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>华实学伴 - 专业的在线学习平台</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome 图标 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/search-section.css">
|
||||
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
{% include 'learning-path.html' %} <!-- 引入learning-path.html -->
|
||||
<section class="search-section">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="search-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="搜索您感兴趣的课程...">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="搜索您的课程...">
|
||||
<button class="btn btn-primary btn-lg" type="button">
|
||||
<i class="fas fa-search me-1"></i> 搜索
|
||||
</button>
|
||||
@@ -29,7 +31,6 @@
|
||||
</div>
|
||||
</section>
|
||||
<div class="dashboard">
|
||||
<h2 class="dashboard-title">—— 我的选课 ——</h2>
|
||||
<div class="course-cards" id="course-cards">
|
||||
<!-- 循环生成课程卡片 -->
|
||||
{% for course in user_selected_course %}
|
||||
@@ -45,7 +46,7 @@
|
||||
<img src="{{course.image_url}}"
|
||||
class="course-img" alt="课程封面">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<span class="course-category">艺术设计</span>
|
||||
<span class="course-category">算法设计</span>
|
||||
<h5 class="course-title">{{course.name}}</h5>
|
||||
<p class="course-instructor">{{course.description}}</p>
|
||||
<div class="course-meta">
|
||||
@@ -53,8 +54,8 @@
|
||||
<span class="course-rating"><i class="fas fa-star"></i> 4.7</span>
|
||||
</div>
|
||||
<div class="mt-auto d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-view flex-fill"><i class="far fa-eye me-1"></i> 查看课程</button>
|
||||
<button class="btn btn-enroll flex-fill"><i class="fas fa-plus me-1"></i> 选课</button>
|
||||
<button class="btn btn-view flex-fill" onclick="btn_view(event,'{{course._id}}')"><i class="far fa-eye me-1"></i> 课程</button>
|
||||
<button class="btn btn-process flex-fill" onclick="openCourseProgress('{{course._id}}')"><i class="fas fa-plus me-1"></i> 章节列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,16 +68,16 @@
|
||||
<!-- 右侧滑出的学习进度卡片 -->
|
||||
<div id="courseProgress" class="course-progress">
|
||||
<div class="progress-header">
|
||||
<span id="course-title"></span>
|
||||
<button class="close-btn" onclick="closeCourseProgress()">×</button>
|
||||
<span id="course-title" class="course-title-in-progress"></span>
|
||||
<button class="close-btn" onclick="closeCourseProgress()"><i class="fas fa-times-circle fa-lg"></i></button>
|
||||
</div>
|
||||
<div class="progress-content">
|
||||
<p>当前学习进度: <span id="progress"></span></p>
|
||||
<hr/>
|
||||
<h3>章节列表:</h3>
|
||||
<ul id="chapter-list">
|
||||
<!-- 章节列表会在点击课程时动态生成 -->
|
||||
</ul>
|
||||
<!-- 当前学习进度 -->
|
||||
<p><span id="progress"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,11 +12,18 @@
|
||||
<script>
|
||||
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/"></script> -->
|
||||
<script src="/static/cdnback/marked.min.js"></script>
|
||||
<!-- <script src="https://cdn.socket.io/4.0.0/"></script> -->
|
||||
<script src="/static/cdnback/socket.io.min.js"></script>
|
||||
<!-- <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/" rel="stylesheet"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/bootstrap.min.css">
|
||||
<!-- <script src="https://unpkg.com/split.js/dist/"></script> -->
|
||||
<script src="/static/cdnback/split.min.js"></script>
|
||||
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/"></script> -->
|
||||
<script src="/static/cdnback/jquery.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/bootstrap-icons.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -3,20 +3,90 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>课程选择主页</title>
|
||||
<link rel="stylesheet" href="/static/css/navbar.css">
|
||||
<link rel="stylesheet" href="/static/css/footer.css">
|
||||
<script src="/static/cdnback/jquery.min.js"></script>
|
||||
|
||||
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/search-section.css">
|
||||
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar">
|
||||
<!-- 原有导航内容保留 -->
|
||||
{% include 'navbar.html' %}
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- 原有主体内容保留 -->
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<main>
|
||||
<section class="search-section">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="search-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="搜索您感兴趣的课程...">
|
||||
<button class="btn btn-primary btn-lg" type="button">
|
||||
<i class="fas fa-search me-1"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="course-selection">
|
||||
<h2>课程列表</h2>
|
||||
<div class="course-cards" id="course-cards">
|
||||
{% for course in courses_data %}
|
||||
<!-- <div class="course-card" onclick="show_course_details('{{ course._id }}')">
|
||||
<img src="{{ course.image_url }}" alt="{{ course.course_name }}">
|
||||
<h3>{{ course.name }}</h3>
|
||||
<p>{{ course.description }}</p>
|
||||
{% if course._id in selected_courses %}
|
||||
<button class="selected-button">已选择</button>
|
||||
{% else %}
|
||||
<button class="select-button" data-course-id="{{course._id}}">选择课程</button>
|
||||
{% endif %}
|
||||
</div> -->
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')">
|
||||
<img src="{{course.image_url}}"
|
||||
class="course-img" alt="课程封面">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<span class="course-category">算法设计</span>
|
||||
<h5 class="course-title">{{course.name}}</h5>
|
||||
<p class="course-instructor">{{course.description}}</p>
|
||||
<div class="course-meta">
|
||||
<span><i class="far fa-user me-1"></i> 初级</span>
|
||||
<span class="course-rating"><i class="fas fa-star"></i> 4.7</span>
|
||||
</div>
|
||||
<div class="mt-auto d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-view flex-fill" onclick="btn_view(event,'{{course._id}}')"><i class="far fa-eye me-1"></i> 课程</button>
|
||||
{% if course._id in selected_courses %}
|
||||
<button class="btn btn-process flex-fill selected-button" data-course-id="{{course._id}}">已选课</button>
|
||||
{% else %}
|
||||
<button class="btn btn-process flex-fill select-button" onclick="on_click_select_course(event)" data-course-id="{{course._id}}">选择课程</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
{% include 'foot.html' %}
|
||||
</body>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/'/g, "\""))
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/index.js"></script>
|
||||
</html>
|
||||
|
||||
|
||||
145
Html/apps/templates/learning-path.html
Normal file
145
Html/apps/templates/learning-path.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<link rel="stylesheet" href="/static/css/learning-path.css">
|
||||
<!-- 学习路径区域 -->
|
||||
<section class="learning-path-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">学习路径</h2>
|
||||
|
||||
<!-- 专业选择器 -->
|
||||
<div class="major-selector">
|
||||
<label for="majorSelect">选择专业方向:</label>
|
||||
<select class="form-select" id="majorSelect">
|
||||
<option value="computer">计算机科学与技术</option>
|
||||
<option value="data">数据科学与大数据</option>
|
||||
<option value="business">工商管理</option>
|
||||
<option value="design">艺术设计</option>
|
||||
<option value="language">语言文学</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 学习路径流程图 -->
|
||||
<div class="path-flow">
|
||||
<div class="path-container">
|
||||
<!-- 阶段1:已完成 -->
|
||||
<div class="path-step step-completed">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-code step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">编程基础</div>
|
||||
<div class="step-courses">3门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<div class="step-status">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段2:已完成 -->
|
||||
<div class="path-step step-completed">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-project-diagram step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">数据结构</div>
|
||||
<div class="step-courses">2门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<div class="step-status">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段3:进行中 -->
|
||||
<div class="path-step step-active">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-brain step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">算法设计</div>
|
||||
<div class="step-courses">3门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 67%"></div>
|
||||
</div>
|
||||
<div class="step-status">进行中</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段4:未开始 -->
|
||||
<div class="path-step">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-database step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">数据库原理</div>
|
||||
<div class="step-courses">2门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="step-status">未开始</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段5:未开始 -->
|
||||
<div class="path-step">
|
||||
<div class="step-connector"></div>
|
||||
<div class="step-node">
|
||||
<i class="fas fa-laptop-code step-icon"></i>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="step-title">Web开发</div>
|
||||
<div class="step-courses">5门课程</div>
|
||||
<div class="step-progress">
|
||||
<div class="step-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="step-status">未开始</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课程详情面板(默认隐藏,点击阶段显示) -->
|
||||
<div class="course-panel" id="algorithm-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">算法设计 - 课程列表</h3>
|
||||
<button class="close-panel"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="course-list">
|
||||
<div class="course-item">
|
||||
<i class="fas fa-check-circle course-check"></i>
|
||||
<div class="course-info">
|
||||
<div class="course-name">算法基础与复杂度分析</div>
|
||||
<div class="course-duration">12课时</div>
|
||||
</div>
|
||||
<div class="course-action">
|
||||
<button class="btn btn-sm btn-primary">继续学习</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-item">
|
||||
<i class="fas fa-check-circle course-check"></i>
|
||||
<div class="course-info">
|
||||
<div class="course-name">排序与搜索算法</div>
|
||||
<div class="course-duration">15课时</div>
|
||||
</div>
|
||||
<div class="course-action">
|
||||
<button class="btn btn-sm btn-primary">继续学习</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-item">
|
||||
<i class="far fa-circle course-check"></i>
|
||||
<div class="course-info">
|
||||
<div class="course-name">动态规划与贪心算法</div>
|
||||
<div class="course-duration">18课时</div>
|
||||
</div>
|
||||
<div class="course-action">
|
||||
<button class="btn btn-sm btn-outline-primary">开始学习</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/learning-path.js"></script>
|
||||
</section>
|
||||
@@ -4,17 +4,32 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>课时编辑</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/lesson.css">
|
||||
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
||||
<link rel="stylesheet" href="/static/css/lesson_setting.css">
|
||||
<link rel="stylesheet" href="/static/css/modelinput.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> -->
|
||||
<script src="/static/cdnback/axios.min.js"></script>
|
||||
<script src="/static/js/modelinput.js"></script>
|
||||
<!-- Markdown Editor (EasyMDE) -->
|
||||
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css" /> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/easymde.min.css" />
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script> -->
|
||||
<script src="/static/cdnback/easymde.min.js"></script>
|
||||
|
||||
<!-- Markdown 渲染与安全 -->
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script> -->
|
||||
<script src="/static/cdnback/markdown-it.min.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js"></script> -->
|
||||
<script src="/static/cdnback/purify.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/lesson_markdown.css">
|
||||
|
||||
</head>
|
||||
<body >
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
@@ -74,6 +89,9 @@
|
||||
<div id="tab-score" class="tab">
|
||||
<textarea id="editor-score" data-type="score"></textarea>
|
||||
</div>
|
||||
<aside class="md-preview-pane" id="md-preview">
|
||||
<div style="color:#64748b">预览区域会实时渲染当前激活的编辑器内容…</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -81,7 +99,9 @@
|
||||
<script src="/static/js/course_current.js"></script>
|
||||
<script src="/static/js/teacherboard.js"></script>
|
||||
<script src="/static/js/teacher_course_setting.js"></script>
|
||||
<script src="/static/js/lesson_markdown.js"></script>
|
||||
<script src="/static/js/lesson.js"></script>
|
||||
|
||||
<script>
|
||||
window.lesson = {{ lesson|tojson|safe }};
|
||||
window.chapter = {{ chapter|tojson|safe }};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<link rel="stylesheet" href="/static/css/navbar.css">
|
||||
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<header class="navbar">
|
||||
<div class="logo">
|
||||
<h1>华实君:教学练评一体的虚拟助教</h1>
|
||||
<h1>华实学伴:教学练评一体的虚拟助教</h1>
|
||||
</div>
|
||||
{% if role == 'teacher' %}
|
||||
<!-- 如果是教师角色 -->
|
||||
<div class="navbar-links">
|
||||
<a href="/teacherindex">首页</a>
|
||||
<a href="/teacherdashboard">课程</a>
|
||||
<a href="/">首页</a>
|
||||
<a href="/teacherboard">课程</a>
|
||||
<a href="/teachergrades">成绩</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
<script>
|
||||
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="/static/cdnback/marked.min.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> -->
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"></script> -->
|
||||
<script src="/static/cdnback/socket.io.min.js"></script>
|
||||
<!-- <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet"> -->
|
||||
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
||||
<link rel="stylesheet" href="/static/css/modelinput.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
|
||||
<script src="/static/js/modelinput.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -4,6 +4,10 @@ from ..services.auth_service import register_user, login_user, logout_user
|
||||
from ..auth.decorators import require_role
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
# 将/favicon.ico重定向到/static/favicon.ico
|
||||
@bp.route('/favicon.ico')
|
||||
def redirect_favicon():
|
||||
return redirect(url_for('static', filename='favicon.ico'))
|
||||
|
||||
@bp.get("/register")
|
||||
def register():
|
||||
|
||||
@@ -5,9 +5,18 @@ from bson import ObjectId
|
||||
from datetime import datetime
|
||||
from ..auth.decorators import require_role
|
||||
from ..services.course_service import load_material,save_materials_markdown_cos
|
||||
from ..services.cos_service import upload_file
|
||||
import uuid
|
||||
|
||||
bp = Blueprint('lesson', __name__)
|
||||
|
||||
@bp.route("/api/upload", methods=["POST"])
|
||||
def upload():
|
||||
f = request.files['file']
|
||||
url = upload_file(f, "image_" + str(uuid.uuid4()))
|
||||
return jsonify({"url": url})
|
||||
|
||||
|
||||
# 返回课时元信息(含三份 markdown 的可访问链接或直接内容)
|
||||
@bp.route('/api/materials/<material_id>/chapters/<chapter_name>/lessons/<lesson_name>', methods=['GET'])
|
||||
@require_role(roles="teacher")
|
||||
|
||||
@@ -28,4 +28,5 @@ region = ap-guangzhou
|
||||
|
||||
[ASE_ENGINE]
|
||||
url = https://asengine.net
|
||||
namespace = /socket.io/c159a41d238b00f35ff33e036d007a0dec4d03197084b5557949a2f7f5ffce03
|
||||
namespace = /socket.io/7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
|
||||
url_token = 7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
|
||||
|
||||
1
Html/db/data/user/_10086_.json
Normal file
1
Html/db/data/user/_10086_.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "_10086_"}
|
||||
1
Html/db/data/user/_10235101550.json
Normal file
1
Html/db/data/user/_10235101550.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "_10235101550"}
|
||||
1
Html/db/data/user/a10235101450.json
Normal file
1
Html/db/data/user/a10235101450.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "a10235101450"}
|
||||
1
Html/db/data/user/chenjianfei.json
Normal file
1
Html/db/data/user/chenjianfei.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "chenjianfei"}
|
||||
1
Html/db/data/user/cjf10235101566.json
Normal file
1
Html/db/data/user/cjf10235101566.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "cjf10235101566"}
|
||||
1
Html/db/data/user/cn0101.json
Normal file
1
Html/db/data/user/cn0101.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "cn0101"}
|
||||
1
Html/db/data/user/ddd.json
Normal file
1
Html/db/data/user/ddd.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "ddd"}
|
||||
1
Html/db/data/user/dhj.json
Normal file
1
Html/db/data/user/dhj.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "dhj"}
|
||||
1
Html/db/data/user/dhj_teacher.json
Normal file
1
Html/db/data/user/dhj_teacher.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "dhj_teacher"}
|
||||
1
Html/db/data/user/dvd.json
Normal file
1
Html/db/data/user/dvd.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "dvd"}
|
||||
1
Html/db/data/user/emmeta.json
Normal file
1
Html/db/data/user/emmeta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "emmeta"}
|
||||
1
Html/db/data/user/exceptedgoat.json
Normal file
1
Html/db/data/user/exceptedgoat.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "exceptedgoat"}
|
||||
1
Html/db/data/user/huo.json
Normal file
1
Html/db/data/user/huo.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "huo"}
|
||||
1
Html/db/data/user/jqs.json
Normal file
1
Html/db/data/user/jqs.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username": "jqs"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user